Descobri o que me incomoda com Lisp-like languages

Não são só os parênteses. É a falta de pistas sobre o significado dos elementos (fora o primeiro de todos) das listas; e também porque tudo “cresce para dentro”, criando mais níveis de aninhamento. Quer uma variável? Põe mais um let ou um destructuring-bind e indente todo o resto do código mais um nível! (estou pensando principalmente em Common Lisp porque foi um dos Lisps que eu “tentei gostar”, mesmo sabendo que não seria o mais elegante de todos).

A falta de pistas é um caso interessante. À primeira vista, a linguagem parece incrivelmente regular: tudo é uma chamada com parâmetros, assim: (chamada param1 param2 param3). Funções, macros, formas especiais (*), tudo igual.

Mas daí você vê que num (if «condição» «código if true» «código if false»), nem tudo tem o mesmo valor. Não dá para começar a ler do meio, porque o terceiro parâmetro do if só executa se a condição for falsa, e não há separador entre as três partes (uma palavrinha “else” seria bem útil). Você vê (a 1) e isso pode ser a chamada da função “a”, ou pode ser a inicialização de uma nova variável, se estiver na posição correta dentro de um “let”, “do”, etc. Isso da “posição correta” é o que complica. Em linguagens como-C (C-like), é mais fácil começar a “ler do meio”. Tem uma palavra “else”? Você está no meio de um “if”, procure-o mais acima. Abriu/fechou {chaves}? Então você está dentro de um bloco, o que em Common Lisp seria um mais ou menos um “progn”. Encontrou um “=”? É uma atribuição a uma variável existente ou a uma variável nova. Você já sabe que o lado esquerdo não terá seu valor calculado, e sim alterado. Não há como surgir uma nova variável numa chamada(normal, com, parâmetros). Encontrou um ponto-e-vírgula? Descanse e comece de novo na próxima linha (descanse = faça um flush mental do seu parser de expressões).

Para um iniciante, no caso do let e suas inúmeras variantes (let*, letrec, aliás, para que tantas?), até dá para administrar, mas em construções usadas com menos freqüência como “do” ou “handler-case” dá um desânimo: quais parênteses são agrupadores da “sintaxe” e quais são parênteses normais?

Eu preferiria que houvesse algum indicativo mais claro para elementos como:

  • Nova variável. Clojure faz isso com colchetes.
  • Código “normal” de tamanho indeterminado. É o chamado “progn implícito” do Common Lisp, que pode surgir a qualquer momento.
  • Decisões. Num cond os blocos de código ficam muito misturados com as condições, bastaria algo como when/then para melhorar a visualização.
  • Símbolos. Em Lisp existe o quote (como em: ‘a), mas como as macros têm liberdade de fazer o que quiserem, e como colocar «’» em tudo ficaria feio de qualquer jeito, o uso do quote é inconsistente. Make-instance usa nomes ‘quotados, diversas outras construções (let, setf, handler-case, destructuring-bind) não usam.
  • Tipos, anotações, valores default e qualquer coisa opcional. Lisps têm a mania de usar mais um nível de parênteses sempre que for necessário adicionar algo que era opcional.
    • Variável num let? Você pode usar simplesmente o nome da sua variável. Ah, você queria inicializá-la, como normalmente é o caso? Então coloque-a entre parênteses junto com o valor.
    • Parâmetro de função? Basta o nome. Ah, você quer colocar um valor default? De novo: parênteses adicionais. Quer colocar um tipo num parâmetro de defmethod? Parênteses.
    • Slot dentro de um defclass? Lista de nomes entre parênteses. Atributos adicionais? Então agrupe cada slot com seus atributos em mais um nível de parênteses.

E não há necessariamente nenhuma lógica na escolha do que vai ser agrupado ou não. As variáveis de um let ficam entre parênteses no primeiro argumento, e o resto é código. Nada impediria que as variáveis e o código fossem separadas por um símbolo, como “in” (ML-like). Isso evitaria vários parênteses duplos. Ou o bloco de código (o progn implícito) é que poderia ter um delimitador (como parênteses ou chaves). Os slots de uma classe ficam entre parênteses, mas poderiam ser separados do resto com uma palavra, como “slots”.

A forma handler-case ilustra diversos pontos que incomodam: o primeiro parâmetro é o código protegido (o “try” de outras linguagens). Ele não é um progn implícito (mas poderia ser, por que não?), então se quiser fazer mais de uma coisa, você precisa colocar seu próprio progn. Depois vêm os tratadores de exceção (os “catch”), cada um entre parênteses. O primeiro item de cada é o nome da condition (sem nenhum indicativo especial, é tudo posicional). Depois, entre parênteses (precisavam de um delimitador, adivinha qual escolheram?), fica o nome da variável que receberá a condition. E então você tem um espaço livre para escrever seu código tratador de exceções usando os parênteses no seu significado normal.

É assim:

(handler-case
    (progn
      (do-stuff)
      (do-more-stuff))
  (some-exception (e)
      (recover se)))

Quando poderia ser:

(handler-case
    :try
      (do-stuff)
      (do-more-stuff)
    :catch some-exception :as e :do
      (recover se))

No exemplo hipotético acima, os parênteses são usados apenas para chamar funções/macros, enquanto que a estrutura dentro do handler-case é marcada de outras formas. Assim some-exception não fica parecendo uma chamada de função, a variável “e” é marcada com :as (que sugere ao leitor a ideia de um nome novo) e os blocos de código são marcados com :try e :catch/:do conforme seu propósito.

Dá para fazer isso com macros, claro que dá. Mas aí fica tão diferente da linguagem, que cada programador acaba tendo seu próprio dialeto. Se eu queria uma linguagem, acabei ganhando um kit de montagem em vez de uma linguagem usável.

O único lugar onde Common Lisp se permite usar algumas palavras a mais é no loop… Na verdade todas as ideias acima foram inspiradas no loop.

(*) Peraí, formas especiais? Pois é, nem Lisp escapa da necessidade de dar valor especial a certas formas. Fico agora pensando se TCL não acaba sendo mais regular…

Anúncios

6 pensamentos sobre “Descobri o que me incomoda com Lisp-like languages

  1. Eu dormi meio mal e acordei há pouco, então vou deixar um comentário das dorgas aqui.

    O flip side de tudo ter a mesma cara independentemente da semântica é que é possível “parsear” (“ler” no linguajar líspico) o código sem conhecer as formas, i.e., eu consigo ler a s-expression de uma forma ‘handler-case’ sem nem saber o que é um handler-case, diferente de uma linguagem com uma sintaxe “normal”, em que eu não vou conseguir parsear um try/catch sem saber de antemão que existem statements try/catch. Isso facilita a vida das macros porque elas já recebem a forma “mastigadinha” ao invés de ter que parsear strings.

    O problema é que o “mastigadinho” é simplesmente uma lista indistinta de coisas aninhadas. Aliás, acho que dá pra dizer que a motivação por trás de diversas escolhas sintáticas do Common Lisp é baseada nas ferramentas que existem pra manipular listas. Por exemplo, implicit progn sempre é a última coisa na forma, porque isso vai ser um ‘tail’ da lista; por isso que o handler-case não usa um implicit progn: o corpo não é a última coisa – diferentemente do handler-bind, a forma almost, but not quite, entirely unlike handler-case, em que tudo é ao contrário, os handlers vão primeiro num argumento só agrupado com parênteses (que nem o let), e o corpo vai no final com implicit progn.

    Muitos mil kalpas atrás eu já pensei que a sintaxe de um Lisp pode se dar ao luxo de ser um pouco mais complexa desde que a linguagem forneça ferramentas para parsear a complexidade. Por exemplo, uma vez eu já pensei em ter uma forma de pattern matching a la:

        (match x
          pattern1 -> body1 ;
          pattern2 -> body2 ;
          else -> body-else)
    

    onde ; seria um símbolo (e a sintaxe de comentário teria que mudar); bastaria ter uma maneira simples de pegar “tudo até o primeiro ;”. Ao mesmo tempo, eu meio que me acostumei com o tempo com os parênteses da vida. Em Scheme, a galera costuma usar colchetes para agrupar coisas que não são formas (e.g., os pares variável/valor dentro do primeiro argumento do let), o que ajuda bastante a leitura.

    As you probably know, faz muitos mil anos que eu venho pensando em criar uma linguagem Lisp-like, e uma situação em que eu não sei o que fazer com os parênteses é como conciliar default values e declarações de tipos em argumentos de função. Talvez a solução seja introduzir um pouco de sintaxe não-parentética mesmo:

        (define (foo ([i : int] [str : string = "hello"]) -> char)
          (elt str i))
    

    I don’t know.

    Quanto às formas especiais, yeah, Tcl escapa delas fazendo tudo em tempo de execução. Compilar eficientemente uma linguagem em que coisas como ‘let’ não são formas especiais parece meio challenging. Existe uma linguagem chamada Kernel (https://web.cs.wpi.edu/~jshutt/kernel.html) em que basicamente tudo é um special operator (e funções são um caso especial), mas acho que não existe (ainda?) uma implementação eficiente da linguagem.

    • Sorry,

      (define (foo [i : int] [str : string = "hello"] -> char)
        (elt str i))
      

      Coloquei um par de parênteses a mais. :P

      • Sobre o par de parênteses a mais: bem, não há nada nos fundamentos dos Lisps que impeça que seu dialeto use esses parênteses a mais, hehehe. Seria uma peculiaridade sintática da sua linguagem e se tornaria mais um pet peeve de muita gente.

        • Os parênteses a mais foram sem querer. É que eu nunca me decidi se prefiro:

          (defun foo ([x : int] [y : string]) -> char
            ...
          

          ou:

          (define (foo [x : int] [y : string] -> char)
            ...)
          

          Eu misturei as duas possibilidades sem querer. Definitivamente tinha um par de parênteses a mais, mas fica em aberto _qual_ par. :P

        • Eu sempre achei defun meio mórbido pois associava com “defunto”, hahaha. Mas uma vez vi uma brincadeira dizendo que Scheme tirou “all de fun” do Common Lisp e comecei a achar mais simpático.

    • (o “comentário das dorgas” está bem compreensível)

      Concordando e acrescentando:

      Sim, a lista indistinta de coisas aninhadas que é o problema. Parsear um Lisp tem a vantagem de você sempre saber onde as estruturas sintáticas começam e terminam, e isso não deve ser mudado. Já o “meio” dessas estruturas já é um vale-tudo atualmente, e acho que poderia ser um vale-tudo mais human-friendly. O parser pode saltar para o fim de um let ou handler-case caso não o conheça, mas não saberá onde há novas variáveis, não saberá qual é a indentação idiomática, e não saberá onde o código “volta ao normal”, com chamadas de função e tudo mais. Tem algo mais que um parser ignorante possa fazer além de “pular para o fim” ou “indentar desconsiderando casos idiomáticos”?

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s