Por favor, explique alguns dos pontos de Paul Graham no Lisp


146

Preciso de ajuda para entender alguns dos pontos de What Made Lisp Different, de Paul Graham .

  1. Um novo conceito de variáveis. No Lisp, todas as variáveis ​​são efetivamente ponteiras. Os valores são o que têm tipos, não variáveis, e atribuir ou vincular variáveis ​​significa copiar ponteiros, não o que eles apontam.

  2. Um tipo de símbolo. Os símbolos diferem das strings, pois você pode testar a igualdade comparando um ponteiro.

  3. Uma notação para código usando árvores de símbolos.

  4. Todo o idioma sempre disponível. Não há distinção real entre tempo de leitura, tempo de compilação e tempo de execução. Você pode compilar ou executar código durante a leitura, ler ou executar código durante a compilação e ler ou compilar código em tempo de execução.

O que esses pontos significam? Como eles são diferentes em linguagens como C ou Java? Algum outro idioma além dos idiomas da família Lisp possui alguma dessas construções agora?


10
Não tenho certeza de que a tag de programação funcional seja justificada aqui, pois é igualmente possível escrever código imperativo ou OO em muitos Lisps, assim como escrever código funcional - e, de fato, há muitos Lisp não funcionais. código ao redor. Eu sugiro que você remova a tag fp e adicione o clojure - espero que isso possa trazer alguma entrada interessante dos Lispers baseados na JVM.
Michał Marczyk

58
Também temos uma paul-grahametiqueta aqui? !!! Ótimo ...
missingfaktor 19/09/10

@missingfaktor Talvez ele precisa de um burninate-request
gato

Respostas:


98

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 (printlne 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 123se tornam o número, 123etc. 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 readou evala 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.


4
Cuidado: ao dizer "macro regular (compilador)", você está quase sugerindo que macros do compilador são macros "regulares", quando no Common Lisp (pelo menos), "macro do compilador" é uma coisa muito específica e diferente: lispworks. com / documentation / lw51 / CLHS / Body /…
Ken

Ken: Boa captura, obrigado! Vou mudar isso para "macro comum", que acho improvável que engane alguém.
Michał Marczyk

Resposta fantástica. Aprendi mais em 5 minutos do que em horas pesquisando / ponderando a questão. Obrigado.
Charlie Flowers

Edit: argh, entendeu mal uma sentença. Corrigido pela gramática (é necessário um "par" para aceitar minha edição).
Tatiana Racheva

Expressões S e XML podem ditar as mesmas estruturas, mas o XML é muito mais detalhado e, portanto, não é adequado como sintaxe.
21413 Sylwester

66

1) Um novo conceito de variáveis. No Lisp, todas as variáveis ​​são efetivamente ponteiras. Os valores são o que têm tipos, não variáveis, e atribuir ou vincular variáveis ​​significa copiar ponteiros, não o que eles apontam.

(defun print-twice (it)
  (print it)
  (print it))

'it' é uma variável. Pode ser associado a QUALQUER valor. Não há restrição e nenhum tipo associado à variável. Se você chamar a função, o argumento não precisará ser copiado. A variável é semelhante a um ponteiro. Tem uma maneira de acessar o valor que está vinculado à variável. Não há necessidade de reservar memória. Podemos passar qualquer objeto de dados quando chamamos a função: qualquer tamanho e qualquer tipo.

Os objetos de dados têm um 'tipo' e todos os objetos de dados podem ser consultados por seu 'tipo'.

(type-of "abc")  -> STRING

2) Um tipo de símbolo. Os símbolos diferem das strings, pois você pode testar a igualdade comparando um ponteiro.

Um símbolo é um objeto de dados com um nome. Normalmente, o nome pode ser usado para encontrar o objeto:

|This is a Symbol|
this-is-also-a-symbol

(find-symbol "SIN")   ->  SIN

Como os símbolos são objetos de dados reais, podemos testar se são o mesmo objeto:

(eq 'sin 'cos) -> NIL
(eq 'sin 'sin) -> T

Isso nos permite, por exemplo, escrever uma frase com símbolos:

(defvar *sentence* '(mary called tom to tell him the price of the book))

Agora podemos contar o número de THE na frase:

(count 'the *sentence*) ->  2

No Common Lisp, os símbolos não apenas têm um nome, mas também podem ter um valor, uma função, uma lista de propriedades e um pacote. Portanto, os símbolos podem ser usados ​​para nomear variáveis ​​ou funções. A lista de propriedades geralmente é usada para adicionar metadados aos símbolos.

3) Uma notação para código usando árvores de símbolos.

O Lisp usa suas estruturas de dados básicas para representar o código.

A lista (* 3 2) pode ser tanto dados quanto código:

(eval '(* 3 (+ 2 5))) -> 21

(length '(* 3 (+ 2 5))) -> 3

A árvore:

CL-USER 8 > (sdraw '(* 3 (+ 2 5)))

[*|*]--->[*|*]--->[*|*]--->NIL
 |        |        |
 v        v        v
 *        3       [*|*]--->[*|*]--->[*|*]--->NIL
                   |        |        |
                   v        v        v
                   +        2        5

4) Todo o idioma sempre disponível. Não há distinção real entre tempo de leitura, tempo de compilação e tempo de execução. Você pode compilar ou executar código durante a leitura, ler ou executar código durante a compilação e ler ou compilar código em tempo de execução.

O Lisp fornece as funções READ para ler dados e código do texto, LOAD para carregar o código, EVAL para avaliar o código, COMPILE para compilar código e PRINT para gravar dados e código no texto.

Essas funções estão sempre disponíveis. Eles não vão embora. Eles podem fazer parte de qualquer programa. Isso significa que qualquer programa pode ler, carregar, avaliar ou imprimir código - sempre.

Como eles são diferentes em linguagens como C ou Java?

Esses idiomas não fornecem símbolos, código como dados ou avaliação de tempo de execução dos dados como código. Os objetos de dados em C geralmente não são digitados.

Algum outro idioma além dos idiomas da família LISP possui alguma dessas construções agora?

Muitos idiomas possuem alguns desses recursos.

A diferença:

No Lisp, esses recursos são projetados na linguagem para facilitar o uso.


33

Para os pontos (1) e (2), ele está falando historicamente. As variáveis ​​de Java são praticamente as mesmas, e é por isso que você precisa chamar .equals () para comparar valores.

(3) está falando sobre expressões S. Os programas Lisp são escritos nesta sintaxe, o que oferece muitas vantagens sobre sintaxe ad-hoc como Java e C, como capturar padrões repetidos em macros de uma maneira muito mais limpa que as macros C ou modelos C ++ e manipular código com a mesma lista principal operações que você usa para dados.

(4) tomando C, por exemplo: a linguagem é realmente duas sub-línguas diferentes: coisas como if () e while (), e o pré-processador. Você usa o pré-processador para evitar ter que se repetir o tempo todo ou para pular código com # if / # ifdef. Mas os dois idiomas são bem separados, e você não pode usar while () em tempo de compilação como você pode #if.

O C ++ torna isso ainda pior com os modelos. Confira algumas referências sobre a metaprogramação de modelos, que fornece uma maneira de gerar código em tempo de compilação e é extremamente difícil para quem não é especialista. Além disso, é realmente um monte de truques e truques usando modelos e macros que o compilador não pode fornecer suporte de primeira classe - se você cometer um erro de sintaxe simples, o compilador não poderá fornecer uma mensagem de erro clara.

Bem, com o Lisp, você tem tudo isso em um único idioma. Você usa o mesmo material para gerar código em tempo de execução ao aprender em seu primeiro dia. Isso não significa que a metaprogramação seja trivial, mas certamente é mais direta com o suporte de linguagem e compilador de primeira classe.


7
Além disso, agora esse poder (e simplicidade) tem mais de 50 anos e é fácil de implementar, para que um programador iniciante possa fazer isso com orientações mínimas e aprender sobre os fundamentos da linguagem. Você não ouviria uma afirmação semelhante de Java, C, Python, Perl, Haskell etc. como um bom projeto para iniciantes!
Matt Curtis

9
Eu não acho que as variáveis ​​Java são como símbolos Lisp. Não há notação para um símbolo em Java, e a única coisa que você pode fazer com uma variável é obter sua célula de valor. Seqüências de caracteres podem ser internadas, mas geralmente não são nomes; portanto, não faz sentido falar se elas podem ser citadas, avaliadas, aprovadas etc.
Ken

2
Mais de 40 anos podem ser mais precisos :), @Ken: Eu acho que ele quer dizer que 1) variáveis ​​não primitivas em java são por referência, o que é semelhante ao lisp e 2) seqüências de caracteres internas em java são semelhantes a símbolos em lisp - é claro que, como você disse, você não pode citar ou avaliar seqüências / códigos internos em Java, portanto eles ainda são bem diferentes.

3
@ Dan - Não tenho certeza quando a primeira implementação foi elaborado, mas o inicial papel McCarthy em computação simbólica foi publicado em 1960.
Inaimathi

Java possui suporte parcial / irregular para "símbolos" na forma de Foo.class / foo.getClass () - ou seja, um objeto Classe <Foo> tipo de tipo é um pouco análogo - assim como valores enum, para um grau. Mas sombras muito mínimas de um símbolo Lisp.
BRPocock

-3

Os pontos (1) e (2) também se encaixariam no Python. Tomando um exemplo simples "a = str (82.4)", o intérprete primeiro cria um objeto de ponto flutuante com o valor 82.4. Em seguida, chama um construtor de strings que retorna uma string com o valor '82 .4 '. O 'a' no lado esquerdo é apenas um rótulo para esse objeto de string. O objeto de ponto flutuante original foi coletado como lixo porque não há mais referências a ele.

No esquema, tudo é tratado como um objeto de maneira semelhante. Não tenho certeza sobre o Common Lisp. Eu tentaria evitar pensar em termos de conceitos de C / C ++. Eles me abrandaram quando eu tentava entender a bela simplicidade de Lisps.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.