Site menu Fumando listas: Lisp
e-mail icon
Site menu

Fumando listas: Lisp

e-mail icon

Um efeito colateral de não ter uma sólida formação acadêmica na área de informática, é o prazer de "descobrir" algumas coisas que a ciência da computação por certo já definiu há meio século. Não é uma vantagem competitiva, mas é divertido.

Uma das coisas que "descobri" foi a "barreira código/dados", análoga à "barreira sangue/cérebro" da medicina. O objetivo deste artigo é falar desta barreira, e da importância de pelo menos conhecer a linguagem Lisp, onde a barreira código/dados não existe.

Mas que droga de barreira é essa? Vou dar um exemplo bem simples: uma função que recebe dois números e uma operação aritmética, e tem de devolver o resultado. Em C:

double math(const char *op, double a, double b)
{
        if (strcmp(op, "mais") == 0) {
                return mais(a, b);
        } else if (strcmp(op, "menos") == 0) {
                return menos(a, b);
        ...
}

A ponte entre a string "mais" (dado) e a função "mais" (código) tem de ser feita pelo programador, ainda que sejam homônimas. (Nem mesmo pude usar switch-case porque o seletor op é uma string.)

Parece um problema de importância menor, mas este divórcio entre dados e código é uma constante pedra no sapato. O pesadelo de qualquer programa C é ter de lidar com dados de tipo variável, porque cada tipo implica em executar código ligeiramente diferente. A "lambança" de bibliotecas como GLib deve-se em grande parte a isto.

Outro desafio frequente, na direção contrária, é a necessidade de "scriptar" um aplicativo. A maioria das linguagens compiladas não é apropriada para isto — ou por falta de interpretador ou porque a linguagem é árida demais para a tarefa.

Qualquer aplicativo que mereça este nome acaba tendo de lidar com estes problemas, a começar pelo carregamento e interpretação de um simples arquivo de configuração. Como diz a décima regra de Greenspun:

Qualquer programa em C ou FORTRAN suficientemente complicado contém uma implementação improvisada, mal especificada, bugada e lenta de metade do Common Lisp.

Na verdade, já passamos deste estágio. É lugar-comum uma aplicação C ou C++ embutir um interpretador de uma linguagem como Javascript ou Lua (linguagens muito mais próximas do Lisp).

Em C++, a barreira código/dados fica um pouquinho mais baixa, pois podemos usar uma matriz associativa e ponteiros de funções:

typedef double (*opfunction)(double, double);
map<string, opfunction> mapa;
mapa["mais"] = &mais;
mapa["menos"] = &menos;
...

double math(const char *op, double a, double b)
{
        return mapa[op](a, b);
}

Mas no fundo é açúcar sintático, o programador continua responsável por construir cada "furo" da barreira. Em Javascript, a barreira é consideravelmente mais fina:


var mapa = {};

mapa.mais = function (a, b) {
        return a + b;
}

mapa["menos"] = function (a, b) {
        return a - b;
}

function math(op, a, b)
{
        return mapa[op](a, b);
}

A função final ficou parecida com C++, mas a forma de construir "mapa" é muito mais direta. Javascript permite atingir e.g. a função "mais" a partir de uma string contendo o nome "mais". Note ainda que a função "menos" foi incluída no mapa de forma diferente, porém equivalente.

Isto tudo é possível porque Javascript usa matriz associativa como pedra-de-canto da própria linguagem, enquanto em C++ o tipo map<> é definido pela STL.

Python é um pouco menos direto que Javascript, mas a "barreira código/dados" é praticamente da mesma "finura".

Porém, em ambas, trata-se de uma "finura" de mão única. É fácil relacionar dados a código (no exemplo, ir de string à função), mas não é tão fácil manipular código como dados.

Você até tem o método toSource(), mas ele devolve o código-fonte, não a representação interna que a máquina virtual realmente utiliza. Para fazer algo útil em cima do código cuspido por toSource(), você teria de reescrever um interpretador Javascript completo...

Neste ponto, Python está mais bem na fita pois tem o módulo dis, que dá acesso aos opcodes da máquina virtual. Isto dá um poder de introspecção bem maior à linguagem:

def plus(a, b):
   return a + b
 
import dis
dis.dis(plus)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD          
              7 RETURN_VALUE        

Resta um pequeno problema (que felizmente não é uma pedra no sapato para a maioria das aplicações): uma nova barreira entre código-fonte e código-máquina. A transformação de um para outro é uma via de mão única: você não pode mexer nos opcodes e "descompilar" o resultado para código-fonte humanamente legível.

(Não que seja completamente impossível. O Valgrind faz isso, e com código binário :)

Outra pequena (ou grande) limitação de quase toda linguagem é a impossibilidade de definir novas primitivas. Python tem "if", "while", "for", mas você não pode criar um "when". Javascript e Ruby mascaram esta limitação por conta da sintaxe de funções anônimas, mas ela continua lá.

Sucede que em Lisp todas estas barreiras e limitações simplesmente não existem! Em Lisp, tudo são listas, seja dado ou código. Segue uma sessão interativa:

>> (+ 1 2)   ; lista que é avaliada imediatamente
3

>> '(+ 1 2)  ; a mesma lista sem ser avaliada
(+ 1 2)

>> (setf a (+ 1 2))   ; atribuir a lista (avaliada) à variavel "a"
3

>> (setf b '(+ 1 2))  ; atribuir a lista (sem avaliar) à "b"
(+ 1 2)

>> b     ; obtém "b"
(+ 1 2)

>> (eval b)  ; avalie "b"
3

>> (setf c '(eval b))    ; uma indireção adicional...
(EVAL B)

>> c
(EVAL B)

>> (eval c)
3

>> (1 2 3 4 5)     ; dá pau porque tenta avaliar e "1" não é função
*** - EVAL: 1 is not a function name; try using a symbol instead

>> '(1 2 3 4 5)    ; isto sim pode ser uma simples lista de números
(1 2 3 4 5)

>> (* 2 5/6)     ; lembra das aulas do primário com frações?
5/3
>> (/ 1/4 2/3)
3/8

Não existe distinção dura entre uma lista de dados e uma de código. Código é uma lista que você avalia, ou resolve. Dados são listas que você não avalia. Mas você pode fazer o contrário: avaliar uma lista de dados. Ou usar como dado uma lista de código. A rigor, Lisp não tem sintaxe.

Mas como fazer loops com "for", "while" etc? Bem, Lisp tem um recurso que nenhuma outra linguagem tem: macros. Ok, C tem macros, mas as macros do C são simples substituições textuais, enquanto as macros Lisp são manipulações elaboradas do código.

Isto permite implementar primitivas no estilo "for", "while" etc. como macros, efetivamente estendendo a linguagem, e convenientemente mantendo esse tipo de coisa fora do interpretador em si.

Lisp define alguns tipos primitivos: números inteiros/racionais (vimos acima), strings, números de ponto flutuante e símbolos.

Símbolos são um bicho familiar a quem programa em Ruby: parecem strings, mas não têm aspas. Em Lisp, variáveis e funções são atreladas a símbolos. Como é de se esperar, a "ponte" entre dados-strings e símbolos é fácil de percorrer:

(defun plus (x y)
	(+ x y))

(defun minus (x y)
	(- x y))

(setf var 3)

>> (symbol-name 'plus)
"PLUS"

; funções nativas também podem entrar nessa
>> (string '+)
"+"

>> (read-from-string "var")
VAR ;
3

; usando intern em vez de read-from-string
>> (+ 1 (eval (intern "VAR")))
4

; A forma de usar uma função é ligeiramente diferente
>> (funcall (read-from-string "PLUS") 1 2)
3

Voltando à história das macros, vou mostrar um caso simples onde a macro "pode mais" que uma função.

Vamos supor que desejemos implementar o operador lógico OR com "curto-circuito", ou seja, avaliando o segundo parâmetro apenas se necessário. Isto permite usar parâmetros com efeitos colaterais, no estilo:

Faça isso OU mostre mensagem de erro
Faça isso E faça aquilo (apenas se "Faça isso" deu certo)

Segue a implementação como função e como macro, e os testes que mostram porque a macro é mais adequada.

; funções-cobaia com efeitos colaterais
(defun verdadeiro () (format t "Verdadeiro ") t)
; lembrando que em Lisp "nil" faz papel de falso e "t" é "verdadeiro"
(defun falso () (format t "Falso ") nil)

(defun funcao-ou (a b)
        (if a
                t
                (if b
                        t
                        nil
                )
        )
)

(defmacro macro-ou (a b)
        `(if ,a             ; note a aspa simples reversa
                t
                (if ,b          ; e as vírgulas frente a "a" e "b"
                        t
                        nil
                )
        )
)

; a função, que não vai curto-circuitar
>>(funcao-ou (verdadeiro) (falso))
Verdadeiro Falso
T

; e a macro, que vai
>>(macro-ou (verdadeiro) (falso))
Verdadeiro
T

A função retorna o valor correto (T) mas não pode curto-circuitar porque ambos os parâmetros são avaliados antes da função ser chamada, aí os efeitos colaterais já aconteceram.

A macro curto-circuita porque... é uma macro, é substituição de código. Na execução, a chamada da macro é substituída por algo semelhante a isto:

; as variáveis "a" e "b" só valem dentro da expressão "let"
(let ((a '(verdadeiro)) (b '(falso)))
        (if (eval a)
                t
                (if (eval b)
                        t
                        nil
                )
        )
)

As variáveis "a" e "b" recebem os parâmetros na forma "virgem" e a avaliação com "eval" só acontece quando realmente necessário.

A propósito, o Lisp padrão já possui as macros "and" e "or". Reimplementei apenas com fins didáticos.

As pedras de canto do Lisp

Assim como Javascript e Python são fortemente baseados em listas e matrizes associativas, Lisp é baseado em listas, que por sua vez são baseadas num tipo bastante primitivo, o cons, que vamos chamar de célula.

A célula é composta de apenas um par de valores primitivos, nem mais nem menos.

>> (cons 1 2)
(1 . 2)

>> (setf a (cons 1 2))
(1 . 2)

>> a
(1 . 2)

>> (cdr a)
2

>> (car a)
1

>> (cons "a" 1)
("a" . 1)

>> (cons 1 NIL)
(1)

Uma lista com mais de dois valores é construída a partir de células, como uma lista encadeada simples:

(1 . (2 . (3 . (4 . 5))))

Uma árvore também pode construída a partir da humilde célula, embora fique humanamente complicada bem depressa.

    4     
  /  \   
 2    6      (4 . ((2 . (NIL . NIL)) . (6 . (NIL . NIL))))

Padronagem recursiva: nó = (chave . conteúdo)
	    	      conteúdo = (nó filho 1, nó filho 2)
		      NIL para terminações

Como disse Paul Graham: se você definir uma linguagem com primitivas equivalentes a cons, cdr, car, eq, cond e uma notação para funções baseada em células, você definiu um dialeto de Lisp, mesmo sem querer.

Mesmo programando em Lisp você não precisa lidar diretamente com células se não quiser. O ponto aqui é a minúscula quantidade de definições pétreas contidas no interpretador.

Ok, eu deveria usar apenas Lisp daqui em diante?

Paul Graham advoga que se use Lisp a sério. Um bom argumento dele é que Lisp já tinha em 1959 todos os recursos que hoje são comemorados como novos: closures, remoção da barreira código/dados, garbage collection etc. Se as melhores linguagens modernas tendem a LISP, por que não dar o último passo?

Já Eric Raymond sugere aprender Lisp para abrir a mente, numa admissão implícita que talvez ele não seja tão prático.

Já John Loser, que muitas vezes tem formação acadêmica, diria que é uma linguagem "fora do mercado" e é perda de tempo completa aprender algo assim, pois não lhe dará um emprego.

Por ora, fico mais com a segunda opinião, e dou minhas razões.

Primeiro, a sintaxe, ou ausência dela, do Lisp é um pouco estranha. Paul Graham diria que isto é cacoete de outras linguagens, mas ele se contradisse um pouco quando propôs um dialeto novo do Lisp (o Arc), que teria açúcar sintático. A liberdade de criar sua própria sintaxe é interessante, mas é um pouco como fazer fogo com dois pauzinhos. É reconfortante saber que é possível, mas na prática usar uma caixa de fósforos é melhor. Esta é minha sensação.

Dependendo da aplicação, a existência de uma boa biblioteca é mais importante que a linguagem. Aí as implementações de Lisp, como Common Lisp, sofrem bastante frente às linguagens de uso mais corrente. Isto é um fato da vida. Eu uso PHP no meu site; é uma linguagem atroz, mas vem soberbamente equipada para lidar com Web. E meu uso de PHP resume-se a gerar cabeçalho e rodapé. O tempo que eu levaria para migrar para e.g. Django é mais bem empregado escrevendo artigos como este.

Ressaltando, isto depende da aplicação. Escrever um simulador sem dependências externas seria uma boa forma de "molhar os pés" em Lisp. Já um programa com muitas dependências implicaria em perder muito tempo em achar os bindings mais apropriados, ou mesmo desenvolvê-los. Mesmo que o tempo de desenvolvimento em si fosse zerado pelo Lisp, lidar com bindings incertos anularia a vantagem.

Felizmente, não estamos mais nos anos 60, e a opção não é mais entre Lisp, Fortran e Cobol. É entre Lisp e linguagens excelentes como Python e Javascript (alguns incluiriam Ruby).

Mas é importante conhecer Lisp, porque é uma forma de enxergar onde as demais linguagens ainda apresentam limitações e erguem barreiras entre código e dados. Eu nunca tinha pensado seriamente na impossibilidade de "reassemblar" opcodes Python para código-fonte — até hoje, quando fui escrever este artigo sobre Lisp.

Com John Loser não posso concordar, porque confunde adequabilidade transitória com suficiência ou perfeição. Só porque PHP é bom para fazer site Web e eu encontro desenvolvedor com baixo salário, isto não torna PHP perfeito e eterno.

e-mail icon