A explicação de Matt é perfeitamente boa - e ele faz uma comparação com C e Java, o que eu não farei -, mas por alguma razão eu realmente gosto de discutir esse mesmo tópico de vez em quando, então - aqui está minha chance em uma resposta.
Nos pontos (3) e (4):
Os pontos (3) e (4) da sua lista parecem os mais interessantes e ainda relevantes agora.
Para entendê-los, é útil ter uma imagem clara do que acontece com o código Lisp - na forma de um fluxo de caracteres digitados pelo programador - a caminho de ser executado. Vamos usar um exemplo concreto:
;; a library import for completeness,
;; we won't concern ourselves with it
(require '[clojure.contrib.string :as str])
;; this is the interesting bit:
(println (str/replace-re #"\d+" "FOO" "a123b4c56"))
Este trecho de código Clojure é impresso aFOObFOOcFOO
. Observe que o Clojure provavelmente não satisfaz totalmente o quarto ponto da sua lista, pois o tempo de leitura não está realmente aberto ao código do usuário; Vou discutir o que significaria que isso não fosse o caso.
Então, suponha que tenhamos esse código em um arquivo em algum lugar e solicitamos ao Clojure para executá-lo. Além disso, vamos supor (por uma questão de simplicidade) que superamos a importação da biblioteca. A parte interessante começa em (println
e termina na extremidade )
direita. Isso é lexado / analisado como seria de esperar, mas já surge um ponto importante: o resultado não é uma representação AST específica do compilador - é apenas uma estrutura de dados Clojure / Lisp regular , ou seja, uma lista aninhada que contém vários símbolos, strings e - nesse caso - um único objeto de padrão regex compilado correspondente ao#"\d+"
literal (mais sobre isso abaixo). Alguns Lisps adicionam suas próprias reviravoltas a esse processo, mas Paul Graham estava se referindo principalmente ao Common Lisp. Nos pontos relevantes para sua pergunta, Clojure é semelhante ao CL.
O idioma inteiro no momento da compilação:
Após esse ponto, todo o compilador lida (isso também seria verdadeiro para um intérprete Lisp; o código Clojure sempre é compilado) são estruturas de dados Lisp que os programadores Lisp estão acostumados a manipular. Nesse momento, uma possibilidade maravilhosa se torna aparente: por que não permitir que os programadores Lisp escrevam funções Lisp que manipulam dados Lisp que representam programas Lisp e produzem dados transformados representando programas transformados, para serem usados no lugar dos originais? Em outras palavras - por que não permitir que programadores Lisp registrem suas funções como plugins de compilador, chamados macros no Lisp? E, de fato, qualquer sistema Lisp decente tem essa capacidade.
Portanto, macros são funções regulares do Lisp operando na representação do programa em tempo de compilação, antes da fase final de compilação, quando o código real do objeto é emitido. Como não há limites para os tipos de macros de código que podem ser executados (em particular, o código que eles executam costuma ser escrito com o uso liberal do recurso de macros), pode-se dizer que "todo o idioma está disponível em tempo de compilação "
O idioma inteiro no momento da leitura:
Vamos voltar a esse #"\d+"
regex literal. Como mencionado acima, isso é transformado em um objeto padrão compilado real no tempo de leitura, antes que o compilador ouça a primeira menção de novo código sendo preparado para compilação. Como isso acontece?
Bem, da maneira como o Clojure é atualmente implementado, a imagem é um pouco diferente da que Paul Graham tinha em mente, embora tudo seja possível com um truque inteligente . Em Common Lisp, a história seria um pouco mais limpa conceitualmente. No entanto, o básico é semelhante: o Lisp Reader é uma máquina de estado que, além de realizar transições de estado e eventualmente declarar se alcançou um "estado de aceitação", cospe estruturas de dados do Lisp que os caracteres representam. Assim, os caracteres 123
se tornam o número, 123
etc. O ponto importante agora é: esta máquina de estados pode ser modificada pelo código do usuário. (Como observado anteriormente, isso é inteiramente verdade no caso de CL; para Clojure, é necessário um hack (desencorajado e não usado na prática). Mas discordo, é o artigo de PG que devo estar elaborando, então ...)
Portanto, se você é um programador do Common Lisp e gosta da ideia de literais vetoriais no estilo Clojure, basta conectar ao leitor uma função para reagir adequadamente a alguma sequência de caracteres - [
ou #[
possivelmente - e tratá-la como o início de um literal de vetor que termina na correspondência ]
. Essa função é chamada de macro de leitor e, assim como uma macro comum, pode executar qualquer tipo de código Lisp, incluindo código que foi escrito com uma notação divertida ativada por macros de leitor registradas anteriormente. Portanto, há todo o idioma no momento da leitura para você.
Embrulhando-o:
Na verdade, o que foi demonstrado até agora é que é possível executar funções regulares do Lisp em tempo de leitura ou compilação; o único passo que é necessário dar a partir daqui para entender como a leitura e a compilação são possíveis no tempo de leitura, compilação ou execução é perceber que a leitura e a compilação são elas mesmas executadas pelas funções do Lisp. Você pode simplesmente ligar read
ou eval
a qualquer momento para ler os dados Lisp dos fluxos de caracteres ou compilar e executar o código Lisp, respectivamente. Essa é toda a linguagem ali, o tempo todo.
Observe como o fato de o Lisp satisfazer o ponto (3) da sua lista é essencial para a maneira como ele consegue satisfazer o ponto (4) - o sabor específico das macros fornecidas pelo Lisp depende muito do código ser representado pelos dados regulares do Lisp, que é algo ativado por (3). Aliás, apenas o aspecto "tree-ish" do código é realmente crucial aqui - você pode ter um Lisp escrito usando XML.