Aprendendo Common Lisp

Minhas anotações no estudo do Common Lisp

Conteúdo

Introdução

Sobre o Lisp

  • É uma linguagem bastante antiga, contemporânea do COBOL e do FORTRAN.
  • Concebida por John McCarthy em 1958.
  • Desenvolvida inicialmente no MIT por outros colaboradores, como:
    • Steve Russel, que implementou o primeiro interpretador Lisp.
    • Timothy P. Hart, que contribuiu para o desenvolvimento do sistema de substituição de símbolos.
    • Marvin Minsky, defensor do uso de Lisp em IA.
    • Paul Abrahams, que colaborou para o refinamento da avaliação recursiva de expressões.
  • Seu nome vem de List Processing.
  • Projetada, primariamente, para o processamento de dados simbólicos.
  • É uma linguagem avaliada.
  • Linguagem formal e matemática.
  • As listas são as estruturas de dados fundamentais da linguagem.
  • Código é tratado como dados e dados podem ser executados como código, possibilitando a metaprogramação.

Uma linguagem expressiva

A principal característica do Lisp não é a infinidade de parêntesis, mas a sua expressividade, pois induz a pessoa programadora a escrever códigos que expressam a intencionalidade de suas ideias com clareza. Essa clareza intencional, em parte, deve-se à sintaxe simples e homoicônica da linguagem (código e dados compartilham a mesma estrutura). Tomando as expressões aritméticas como exemplo, na maioria das outras linguagens elas poderiam ser escritas assim (notação infixa):

2 + 3 * 4

Deixando por conta das convenções de precedência e associação de operadores a resolução das ambiguidades. Em Lisp, porém, não há ambiguidades a serem resolvidas (notação prefixa):

(+ 2 (* 3 4))

Basta conhecer a regra da precedência de parêntesis para saber que a intenção de quem escreveu esse código era somar a 2 o resultado da multiplicação de 3 por 4. Nem mesmo o fato dos operadores aparecerem na frente dos operandos causa confusão: até porque não são operadores – são funções. Mas este não é o ponto: o importante é notar que, mesmo se lidos por nós como operadores, a ordem em que os símbolos + e * aparecem entre seus respectivos parêntesis expressa com clareza o que terá que ser feito e com quem.

Dialetos Lisp

Em muitos aspectos, o Lisp se aproxima mais de um conjunto de conceitos e especificações do que daquilo que nós costumamos conceber como "linguagens de programação". Tanto que, desde que uma linguagem respeite esse conjunto de princípios, ela poderá ser considerada um "dialeto Lisp". De fato, podemos entender o Lisp como uma família de linguagens de programação que compartilham uma série de conceitos fundamentais, como listas, funções de alta ordem, e a capacidade de manipular código como dados.

Existem centenas de dialetos Lisp, mas dois deles têm presença predominante nas escolhas da comunidade:

ANSI Common Lisp (CL)
Mais relacionada com os primeiros dialetos Lisp, é uma linguagem rica em funcionalidades com grande foco na objetividade do código.
Scheme
Criada nos anos 1970, é uma linguagem que tende a ser relativamente mais verbosa, já que valoriza muito mais as natureza matemática do Lisp, bem como sua origem acadêmica.

Um dialeto Lisp que conquistou popularidade e bastante relevância no desenvolvimento de aplicações comerciais foi o Clojure. Sendo construído sobre a plataforma Java, o Clojure é capaz de manter o princípio Lisp do "código como dado", ao mesmo tempo em que tem acesso nativo às bibliotecas Java e facilita a criação de programas executáveis em múltiplas threads.

No campo dos dialetos voltados a scripts, nós temos o já mencionado Emacs Lisp, dedicado à customização e expansão das funcionalidades do Emacs, mas há dois outros dialetos bastante conhecidos na comunidade GNU, ambos com origens no Scheme:

Guile Scheme
utilizado em vários softwares livres e na configuração do gerenciador de pacotes e sistema operacional GNU Guix.
Script Fu Scheme
Utilizado na criação de extensões para o editor de imagens GIMP.

ANSI Common Lisp (CL)

Nossos estudos serão sobre o dialeto ANSI Common Lisp, que é uma especificação desenvolvida entre os anos 1981 e 1986 a partir da necessidade de unificar diversos dialetos muito semelhantes em torno de um padrão comum. Deste modo, muitos compiladores foram adaptados às novas especificações e acabaram por consolidar o CL como o dialeto Lisp mais popular até hoje.

Uma das características mais importantes do Common Lisp é que as especificações foram projetadas para que a linguagem fosse multi paradigma, ou seja, capaz de suportar diversos paradigmas de programação, como orientação a objetos, programação funcional, entre outros.

Compiladores

Existem excelentes compiladores livres para o Common Lisp, quando avaliados pelas suas características técnicas. Por muito tempo, houve uma certa disputa quanto a qual seria o "melhor" compilador para aprender CL, em que dois deles dividiam opiniões: o GNU CLISP e o SBCL. Segundo o site Common Lisp Brasil, porém, o CLISP teria se tornado obsoleto em 2010 (o que parece ser o caso, visto que a versão distribuída pelo Debian ainda é a mesma de 2010): e foi isso que me fez optar pelo SBCL para esta série de estudos.

Quando usado em conjunto com o Emacs (via SLY), o SCBL funciona como um "servidor de linguagem", fornecendo suporte à edição do código, depuração integrada, compilação interativa e visualização automatizada de documentação.

Por que aprender Lisp

A decisão de aprender algo novo, especialmente linguagens de programação, geralmente é justificada pela combinação de uma ou mais dessas motivações:

  • Porque é essencial para o progresso em uma certa carreira.
  • Porque é essencial para ingressar em uma certa carreira.
  • Porque é essencial para dominar um certo sistema computacional.
  • Porque todo mundo conhece (e ninguém quer ficar de fora da conversa).

Mas Lisp não é uma linguagem popular, não é fácil de aprender, está longe das 10 linguagens mais requisitadas nas entrevistas de emprego e a maioria das carreiras no mundo pós-moderno são "não carreiras". Mesmo assim, eu prefiro concordar com a criadora de uma das primeiras linguagens de programação de alto nível da história (Flow-Matic):

A frase mais perigosa da linguagem é: "nós sempre fizemos desse jeito".

– Grace Hopper

É fato que quase nada tem um potencial transformador tão grande quanto aprender uma nova linguagem, só que isso não diz nada sobre o por quê de alguém decidir aprender Lisp em vez de outra linguagem qualquer…

Mas eu posso falar por mim:

Emacs
Tendo me interessado em adotar o Emacs como a minha plataforma integrada de produção, não demorei muito para compreender que teria que ter um mínimo de conhecimento da linguagem elisp (Emacs Lisp) para extrair o máximo de suas potencialidades. O elisp é um dialeto Lisp, mas é uma linguagem que só pode ser utilizada para desenvolver aplicações para uso no próprio Emacs. Tudo bem que nós, pessoas utilizadoras do Emacs,tendemos a considerá-lo um sistema operacional completo e autossuficiente, mas esse não é bem o meu caso.
Plataforma Shell Unix/GNU
Foi o meu entusiasmo pelo Projeto GNU que me levou ao Emacs (para mim, o mais GNU dos GNUs), mas também me levou a anos de aprendizado dos mecanismos e funcionalidades do shell (Bash) e da plataforma Unix, tal como implementada no GNU/Linux, como um todo. Então, me parecia um pouco frustrante aprender uma linguagem com a qual eu não pudesse criar programas para usar na boa e velha linha de comandos do shell. Eu até poso invocar o Emacs sem configurações (com os argumentos -Q --script ARQUIVO.el) para executar códigos em elisp, mas seria como matar uma mosca com tiros de canhão. Então, por que não aprender a linguagem da qual o elisp deriva?
Linguagens de programação
Da exploração do sistema GNU para o interesse em como sistemas são programados, é um salto na direção do mais abstraído para níveis cada vez menores de abstração. Assim, eu fui do Bash para o C, do C para o assembly e do assembly surgiu meu interesse pela própria criação de linguagens de programação. Ainda estou tateando esse novo mundo, mas o Lisp, desde o início, chamou muito a minha atenção, não só por ter influências presentes em várias linguagens, mas principalmente por, ela mesma, ter originado muitas outras do modo mais inusitado: pela simples manipulação dos princípios e elementos do próprio Lisp!

No geral, é comum que as pessoas programadoras descrevam suas experiências com o Lisp como "reveladoras". A simplicidade e a beleza da linguagem, bem como seu poder, podem não ser tão profusamente aplicadas em termos práticos ou requeridas nas entrevistas de emprego, mas são inspiradoras na compreensão do que é uma linguagem de programação.

Ambiente de estudos

Requisitos

  • Emacs vanilla: instalar o sly.
  • Doom Emacs: ativar módulo common-lisp
  • GNU/Linux: sbcl (REPL)
  • GNU/Linux: rlwrap (para editar linhas do scbl)
  • Instalar o quicklisp (distribuição de pacotes de bibliotecas)

Instalação do SBCL e do quicklisp

Atalho para o SBCL

alias sbcl='rlwrap sbcl'

Download do quicklisp

:~$ mkdir -p ~/projects/quicklisp
:~$ cd ~/projects/quicklisp
:~/projects/quicklisp$ curl -O https://beta.quicklisp.org/quicklisp.lisp

Instalação do quicklisp

:~/projects/quicklisp$ sbcl --load quicklisp.lisp

No SBCL:

(quicklisp-quickstart:install :path "~/projects/quicklisp")

...

  ==== quicklisp installed ====

    To load a system, use: (ql:quickload "system-name")

    To find systems, use: (ql:system-apropos "term")

    To load Quicklisp every time you start Lisp, use: (ql:add-to-init-file)

    For more information, see http://www.quicklisp.org/beta/

NIL

Depois de instalado, o manual do quicklisp sugere testar a instalação de um pacote:

(ql:system-apropos "vecto")

...

(ql:quickload "vecto")

Se tudo correr bem, é só incluir o quicklisp no arquivo de início do SBCL e sair:

(ql:add-to-init-file)

...

(quit)

Depois deste procedimento, ao abrir um arquivo .lisp no Emacs, o SLY poderá ser conectado ao SBCL. No Doom Emacs, nada mais precisará ser feito, mas em outros tipos de configuração do Emacs, pode ser necessário informar o caminho do binário do SBCL para o SLY:

(use-package sly
  :config (setq inferior-lisp-program "/usr/bin/sbcl"))

Material para pesquisar e aprender

Comunidades

Na web

A linguagem

Terminologia essencial

Dados
Elementos da linguagem que representam as informações a serem manipuladas ou processadas.
Funções (procedures ou procedimentos)
Construções que definem operações que podem ser aplicadas a dados, retornando resultados com base na lógica de um conjunto de regras especificado.
Listas
Estruturas de dados que consistem em uma sequência de elementos agrupados entre parênteses, onde cada elemento pode ser um átomo ou outra lista.
Átomos
Qualquer expressão que não é uma lista, geralmente um símbolo ou um valor literal.
Expressões Simbólicas (SEXP)
Construções da linguagem que podem representar dados ou código, incluindo átomos e listas.
Símbolo
Tipo complexo de dado que pode ser associado a funções, valores, propriedades ou outros objetos (como pacotes).
Forma (form)
Lista que possui um símbolo (geralmente o identificador de uma função ou de uma macro) como primeiro elemento. De modo geral, é qualquer estrutura que pode ser avaliada pelo interpretador Lisp.
Pacote
Um espaço de nomes que agrupa símbolos e seus respectivos valores, permitindo a organização e o controle de identificadores e evitando colisões de nomes entre diferentes partes de um programa.
Sistema
Conjunto organizado de pacotes, descrições, dependências e metadados de um projeto em Lisp.

Tipos de dados

Tipos de Dados Primitivos

Tipos de dados diretamente suportados pela linguagem, ou seja, não requerem construções adicionais para serem utilizados.

  • Números
    • Inteiros (integer)
    • Números de ponto flutuante (float)
    • Números racionais (rational)
    • Números complexos (complex)
  • Caracteres (character)
  • Strings (string)
  • Símbolos (symbol)
  • Números de ponto fixo (fixnum)

Tipos de Dados Derivados ou Construídos

Tipos de dados construídos a partir da combinação dos tipos primitivos.

  • Listas (list)
  • Vetores (vector)
  • Estruturas (structure)
  • Pacotes (package)
  • Funções (function)
  • Streams (stream)
  • Objetos de Entrada/Saída (io)
  • Objetos Especiais
    • nil (representa falso)
    • t (representa verdadeiro)
  • Número de Ponto Fixo (fixnum)

Os tipos de dados mais básicos

Neste primeiro momento, vamos nos concentrar em apenas alguns tipos de dados:

  • Números:
    • Inteiros
    • Ponto flutuante
    • Racionais
  • Strings
  • Símbolos
  • Objetos especiais t e nil
  • Funções

Números

Inteiros

Números sem a parte decimal, incluindo números positivos, negativos e o zero (0).

Exemplo:

CL-USER> 123
123 (7 bits, #x7B, #o173, #b1111011

A avaliação, acima, mostra que os inteiros também podem ser expressos em outras bases de numeração:

  • Representação binária (base 2): #b1111011
  • Representação octal (base 8): #o173
  • Representação hexadecimal (base 16): #x7B

Além disso, o interpretador mostrou a quantidade de bits que o número ocupa (7 bits), o que demonstra a diferença entre o conceito de inteiro no Lisp e em linguagens como o C, por exemplo, onde os tipos representam, essencialmente, o tamanho do espaço reservado para os dados na memória.

Ponto flutuante

Números com ponto flutuante são identificados por um ponto entre os dígitos e/ou com notação exponencial.

Exemplo:

CL-USER> 1.23
1.23 (123.0%)

Notação exponencial:

CL-USER> 1e+10
1.0e10
Racionais

É o conjunto dos números racionais, que inclui inteiros e números expressos por frações.

Exemplo:

CL-USER> 84/126
2/3 (0.6666667, 200/3%)

A fração sempre será avaliada na sua forma mais reduzida.

Strings

As strings são escritas entre aspas duplas.

Exemplo:

CL-USER> "salve, simpatia!"
"salve, simpatia!"

Símbolos

Tipo complexo de dados nomeados que podem estar associados a valores, funções, macros, propriedades ou pacotes.

CL-USER> 'um-símbolo
UM-SÍMBOLO

Objetos especiais T e NIL

O objeto especial t expressa verdadeiro (true):

CL-USER> t
T

O objeto especial nil expressa uma lista vazia, o que é análogo ao valor falso.

CL-USER> nil
NIL

Listas

Principais tipos de dados do Lisp.

LISP vem de "LISt Processing", e não de "Lots of Insane Silly Parenthesis".

Listas são escritas entre parêntesis, mas…

  • (a b c d): Isto é um código que avalia o símbolo a (presumidamente, uma função) com os argumentos b, c e d.
  • '(a b c d): Este é um símbolo que contém os elementos a, b, c e d.

A avaliação do símbolo acima é uma lista:

Exemplo:

CL-USER> '(a b c d)
(A B C D)

Extraindo o terceiro elemento da lista:

Exemplo:

CL-USER> (nth 2 '(a b c d))
C

Notação prefixa ("polonesa")

O operador (função ou macro) vem antes dos operandos (argumentos).

Exemplo:

CL-USER> (+ 13 29)
42 (6 bits, #x2A, #o52, #b101010)

Não há ambiguidade nas operações…

Exemplo:

CL-USER> (+ (* 12 3) 6)
42 (6 bits, #x2A, #o52, #b101010)

Funções

Definindo funções

As funções são definidas com defun.

Exemplo:

(defun soma-dois (a b)
  "Soma dois números."
  (+ a b))

Saída:

SOMA-DOIS

Onde:

  • defun: Define a função.
  • soma-dois: Nome da função (não é avaliado na definição).
  • (a b): Lista de argumentos (não é avaliada na definição).
  • "Soma dois números": Descrição (documentação) opcional da função.
  • (+ a b): Corpo da função e valor retornado.

Chamando a função:

Exemplo:

(soma-dois 3 4)

Saída:

7 (3 bits, #x7, #o7, #b111)