Elmord's Magic Valley

Software, lingüística e rock'n'roll. Às vezes em Português, sometimes in English.

Fazer é fácil, difícil é saber o que fazer

2012-10-12 02:27 -0300. Tags: comp, prog, pldesign, em-portugues

Criar uma linguagem de programação é ao mesmo tempo mais fácil e mais difícil do que parece à primeira vista. A princípio, implementar um compilador ou um interpretador para uma linguagem de programação razoavelmente útil parece uma tarefa complicada. À medida em que nos familiarizamos com o assunto, a tarefa parece cada vez mais simples. Não precisamos nos preocupar com eficiência nas versões iniciais; podemos escrever um compilador simples, que gere código da maneira mais direta (e ineficiente) possível, e depois podemos reescrevê-lo aos poucos para que gere código melhor. Parece simples, não?

E de fato é, em grande parte. Na verdade, implementar é a parte mais fácil. A parte realmente difícil de se criar uma linguagem é o design, e esta é uma coisa da qual eu nunca tinha me dado conta até tentar eu mesmo criar uma linguagem não-trivial1. Criar uma linguagem "igual às outras" é fácil; criar uma linguagem com características que a façam valer a pena em comparação com as linguagens existentes não é uma tarefa trivial.

Há pouco mais de um ano, eu resolvi que iria criar uma linguagem Lisp-like que resolveria todos os problemas do Common Lisp. O Common Lisp existe há quase trinta anos, todo o mundo sabe que a linguagem tem problemas2, e no entanto até hoje ninguém apareceu com uma alternativa compelling. Está mais do que na hora de alguém fazer isso, não?3 Eu resolvi tentar. Na pior das hipóteses, o projeto seria divertido e eu aprenderia alguma coisa com ele.

E de fato eu aprendi. Na verdade, fora alguns experimentos simples, eu quase não escrevi código nessa história. Ao invés de criar o Next Great Lisp, eu descobri que a tarefa é muito mais difícil do que parece, e ao mesmo tempo me dei conta dos porquês de diversas decisões de design do Common Lisp. Sim, Common Lisp é bizarro em vários aspectos, mas é fantástico como as diversas bizarrices interagem entre si para criar um todo mais ou menos coerente. Essa coesão não é muito óbvia, e só é realmente apreciada quando se pára para pensar a fundo sobre as coisas. Tente mudar um aspecto do design da linguagem, e a mudança implica mudar outra coisinha, que implica mudar uma terceira, e assim por diante. Algumas mudanças levam a uma reação em cadeia que acaba levando a linguagem na direção do Scheme. Algumas produzem conflitos difíceis de resolver. Algumas envolvem fazer algum tradeoff em que não é claro que caminho seguir. Enfim, não é simples. Explicar os problemas que eu encontrei exigiria explicar alguns aspectos das linguagens Lisp que talvez não sejam familiares à maioria dos leitores (fica para outro post, quem sabe); ao invés disso, oferecerei um exemplo que há de ser mais compreensível.

Recentemente andei ideando uma outra linguagem (Lisp-like, for sure). A idéia é que a linguagem fosse:

Em uma linguagem dinamicamente tipada, em geral, todo valor carrega consigo uma tag que indica seu tipo, de modo que o tipo pode ser descoberto em tempo de execução. Tipicamente, em uma arquietura de 32 bits, números inteiros simples são representados com (por exemplo) 30 bits de valor, mais 2 bits de tag, que indicam que o valor é um inteiro, e não um ponteiro ou um caractere. Operações aritméticas têm que levar em conta que os dois bits inferiores do valor são a tag, e devem ser isolados na realização da operação e mantidos no resultado. Essas operações a mais sobre o número para gerenciar a tag implicam alguma perda em performance. (Um inteiro simples com tag é conhecido como um fixnum no mundo Lisp e em Ruby.)

A idéia é que, na nossa linguagem, se o tipo de uma variável não for declarado, assume-se que ela pode conter qualquer coisa, e portanto os dados têm que carregar uma tag. Mas se a variável for explicitamente marcada com um tipo (digamos, int32), sabe-se de antemão que a variável só pode conter dados daquele tipo, e portanto uma representação "bruta" (sem tag) pode ser usada. O ganho é tanto em performance quanto em interoperabilidade com funções escritas em C, que trabalham com valores brutos. Além disso, podemos usar todos os 32 bits do valor para guardar o número, o que aumenta a faixa de representação.

Agora, se queremos ter tipos correspondentes aos do C, teremos os tipos int32 e int64 (equivalentes ao int e long long int). Se queremos manter as coisas compatíveis com o C, um número como 4 pertence a ambos os tipos. A questão é: 4:int32 e 4:int64 são valores distintos? Em uma linguagem dinamicamente tipada, o ideal é que a resposta fosse não: tudo o que importa é o valor 4. Se eu escrevo uma função:

(defun square (x: int32) -> int32
  (* x x))

eu espero que ela aceite qualquer inteiro na faixa de representação de um int32. Se o meu 4 é o resultado de uma expressão que retorna um int32 ou um int64, ou mesmo um inteiro com tag, pouco deveria importar.

(defun whatever (x:int32, y:int64, z)
  (print (square x))
  (print (square y))
  (print (square z)))

(whatever 4 4 4)

Assim, int32 é um subtipo de int64. Mas será que é isso que queremos? Imagine que nós tenhamos uma generic function:

(defun print-hex (x: int32)   (printf "%8x\n" x))
(defun print-hex (x: int64)   (printf "%16lx\n" x))

Isto é, queremos imprimir uma representação em hexadecimal dos bytes que compõem o número; o número de bytes a serem impressos depende do tipo do número. Agora, se temos o seguinte código:

(defun whatever (x: int64)
  (print-hex x))

(whatever 4)

Qual dos métodos de print-hex será chamado? Se int32 é um subtipo de int64, então o método que recebe int32 é mais específico do que o método com int64; logo, (print-hex 4) deverá sempre cair na versão com int32, o que pode não ser o comportamento desejado.

Por outro lado, se decidimos que é o método com int64 que deveria ser chamado, estamos decidindo que 4:int32 e 4:int64 são valores distintos. Isso implica que a função square apresentada anteriormente não pode ser chamada com um 4:int64, o que é um tanto quanto desagradável.

Possíveis soluções:

E é assim a vida do desenvolvedor de linguagens de programação.

Notas

1 Hatter não conta como não-trivial; a linguagem é bem simples, programar nela é que não é.

2 Tenho a impressão de que na comunidade Common Lisp existe um consenso geral de que a linguagem tem problemas e que poderia ser melhor, mas ainda assim ela é melhor do que as alternativas.

3 Tentativas não faltam. Quão compelling são os resultados é subjetivo.

Comentários / Comments (0)

Deixe um comentário / Leave a comment

Main menu

Posts recentes

Comentários recentes

Tags

em-portugues (213) comp (137) prog (68) in-english (50) life (47) pldesign (35) unix (34) lang (32) random (28) about (27) mind (25) lisp (23) mundane (22) fenius (20) ramble (17) web (17) img (13) rant (12) hel (12) privacy (10) scheme (10) freedom (8) copyright (7) bash (7) music (7) academia (7) lash (7) esperanto (7) home (6) mestrado (6) shell (6) conlang (5) emacs (5) misc (5) latex (4) editor (4) book (4) php (4) worldly (4) android (4) politics (4) etymology (4) wrong (3) security (3) tour-de-scheme (3) kbd (3) c (3) film (3) network (3) cook (2) poem (2) physics (2) wm (2) treta (2) philosophy (2) comic (2) lows (2) llvm (2) perl (1) en-esperanto (1) audio (1) old-chinese (1) kindle (1) german (1) pointless (1) translation (1)

Elsewhere

Quod vide


Copyright © 2010-2020 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.