Como fazer uma linguagem homoicônica

16

De acordo com este artigo, a seguinte linha de código Lisp imprime "Hello world" na saída padrão.

(format t "hello, world")

O Lisp, que é uma linguagem homoicônica , pode tratar o código como dados da seguinte maneira:

Agora imagine que escrevemos a seguinte macro:

(defmacro backwards (expr) (reverse expr))

para trás é o nome da macro, que pega uma expressão (representada como uma lista) e a inverte. Aqui está "Olá, mundo" novamente, desta vez usando a macro:

(backwards ("hello, world" t format))

Quando o compilador Lisp vê essa linha de código, ele olha para o primeiro átomo na lista ( backwards) e percebe que ele nomeia uma macro. Ele passa a lista não avaliada ("hello, world" t format)para a macro, que reorganiza a lista para (format t "hello, world"). A lista resultante substitui a expressão de macro e é o que será avaliado em tempo de execução. O ambiente Lisp verá que seu primeiro átomo ( format) é uma função e a avaliará, passando o restante dos argumentos.

No Lisp, é fácil realizar essa tarefa (corrija-me se estiver errado) porque o código é implementado como lista ( expressões-s ?).

Agora dê uma olhada neste trecho do OCaml (que não é uma linguagem homoicônica):

let print () =
    let message = "Hello world" in
    print_endline message
;;

Imagine que você deseja adicionar homoiconicidade ao OCaml, que usa uma sintaxe muito mais complexa em comparação com o Lisp. Como você faria isso? A linguagem precisa ter uma sintaxe particularmente fácil para alcançar a homoiconicidade?

EDIT : a partir deste tópico , encontrei outra maneira de obter homoiconicidade diferente da Lisp: a implementada na linguagem io . Pode parcialmente responder a esta pergunta.

Aqui, vamos começar com um bloco simples:

Io> plus := block(a, b, a + b)
==> method(a, b, 
        a + b
    )
Io> plus call(2, 3)
==> 5

Ok, então o bloco funciona. O bloco positivo adicionou dois números.

Agora vamos fazer uma introspecção sobre esse pequeno companheiro.

Io> plus argumentNames
==> list("a", "b")
Io> plus code
==> block(a, b, a +(b))
Io> plus message name
==> a
Io> plus message next
==> +(b)
Io> plus message next name
==> +

Molde frio santo quente. Você não pode apenas obter os nomes dos parâmetros do bloco. E você não apenas pode obter uma sequência do código fonte completo do bloco. Você pode se infiltrar no código e percorrer as mensagens dentro. E o mais surpreendente de tudo: é incrivelmente fácil e natural. Fiel à missão de Io. O espelho de Ruby não pode ver nada disso.

Mas, whoa whoa, ei agora, não toque nesse botão.

Io> plus message next setName("-")
==> -(b)
Io> plus
==> method(a, b, 
        a - b
    )
Io> plus call(2, 3)
==> -1
incudir
fonte
11
Você pode querer ter um olhar em como Scala fez suas macros
Bergi
11
@ Bergi Scala tem uma nova abordagem para macros: scala.meta .
Martin Berger
Eu sempre achei que a homoiconicidade é superestimada. Em qualquer linguagem suficientemente poderosa, você sempre pode definir uma estrutura em árvore que espelha a estrutura da própria linguagem, e funções utilitárias podem ser escritas para traduzir de e para o idioma de origem (e / ou um formulário compilado) conforme necessário. Sim, é um pouco mais fácil nos LISPs, mas, como (a) a grande maioria do trabalho de programação não deve ser metaprogramada e (b) o LISP sacrificou a clareza da linguagem para tornar isso possível, não acho que a troca valha a pena.
Periata Breatta
@PeriataBreatta Você está certo, mas a principal vantagem do MP é que o MP permite abstrações sem penalidade no tempo de execução . Assim, o MP resolve a tensão entre abstração e desempenho, embora com o custo de aumentar a complexidade da linguagem. Vale a pena? Eu diria que o fato de que todos os principais PLs possuem extensões MP indica que muitos programadores que trabalham consideram úteis as ofertas de troca que o MP oferece.
Martin Berger

Respostas:

10

Você pode tornar qualquer idioma homoicônico. Essencialmente, você faz isso 'espelhando' a linguagem (ou seja, para qualquer construtor de linguagem, você adiciona uma representação correspondente desse construtor como dados, pense em AST). Você também precisa adicionar algumas operações adicionais, como citar e não citar. É mais ou menos isso.

O Lisp teve isso desde o início por causa de sua sintaxe fácil, mas a família de idiomas MetaML de W. Taha mostrou que é possível fazer isso em qualquer idioma.

Todo o processo é descrito em Modelando metaprogramação generativa homogênea . Aqui está uma introdução mais leve ao mesmo material .

Martin Berger
fonte
11
Corrija-me se eu estiver errado. "espelhamento" está relacionado à segunda parte da questão (homoiconicidade em io lang), certo?
incorreto 14/09/16
@ Ignus Não tenho certeza se entendi completamente sua observação. O objetivo da homoiconicidade é permitir o tratamento do código como dados. Isso significa que qualquer forma de código deve ter uma representação como dados. Existem várias maneiras de fazer isso (por exemplo, quase-aspas ASTs, usando tipos para distinguir código de dados, como é feito pela abordagem de teste modular leve), mas todas requerem uma duplicação / espelhamento da sintaxe da linguagem de alguma forma.
Martin Berger
Presumo que @Ignus se beneficiaria de olhar para MetaOCaml então? Ser "homoicônico" significa apenas ser cotável então? Presumo que linguagens de vários estágios, como MetaML e MetaOCaml, vão mais longe?
Steven Shaw
11
@StevenShaw MetaOCaml é muito interessante, especialmente o novo BER MetaOCaml da Oleg . No entanto, é um pouco restrito, pois executa apenas metaprogramação em tempo de execução e representa o código apenas por quase-aspas, que não é tão expressivo quanto os ASTs.
Martin Berger
7

O compilador Ocaml é escrito no próprio Ocaml; portanto, certamente existe uma maneira de manipular os ASTs do Ocaml no Ocaml.

Pode-se imaginar adicionar um tipo embutido ocaml_syntaxao idioma e ter uma defmacrofunção embutida, que recebe uma entrada do tipo, digamos

f : ocaml_syntax -> ocaml_syntax

Agora, qual é o tipo de defmacro? Bem, isso realmente depende da entrada, pois, mesmo que fseja a função de identidade, o tipo da parte do código resultante depende da parte da sintaxe transmitida.

Esse problema não surge no lisp, pois o idioma é digitado dinamicamente e nenhum tipo precisa ser atribuído à própria macro no momento da compilação. Uma solução seria ter

defmacro : (ocaml_syntax -> ocaml_syntax) -> 'a

o que permitiria que a macro fosse usada em qualquer contexto. Mas isso é inseguro, é claro, permitiria que um boola fosse usado em vez de um string, travando o programa em tempo de execução.

A única solução baseada em princípios em uma linguagem de tipo estatico seria ter tipos dependentes nos quais o tipo de resultado defmacroseria dependente da entrada. As coisas ficam bastante complicadas neste momento, e eu começaria apontando a agradável dissertação de David Raymond Christiansen.

Concluindo: ter uma sintaxe complicada não é um problema, pois há muitas maneiras de representar a sintaxe dentro da linguagem e, possivelmente, usar a metaprogramação como uma quoteoperação para incorporar a sintaxe "simples" ao interno ocaml_syntax.

O problema está tornando isso bem digitado, principalmente com um mecanismo de macro em tempo de execução que não permite erros de tipo.

É claro que é possível ter um mecanismo de tempo de compilação para macros em uma linguagem como o Ocaml, veja, por exemplo, MetaOcaml .

Também possivelmente útil: Jane street em metaprogramação em Ocaml

cody
fonte
2
MetaOCaml possui meta-programação em tempo de execução, não meta-programação em tempo de compilação. O sistema de digitação do MetaOCaml também não possui tipos dependentes. (O MetaOCaml também foi considerado insensível ao tipo!) O modelo Haskell tem uma abordagem intermediária interessante: todo estágio é seguro para o tipo, mas ao entrar em um novo estágio, precisamos refazer a verificação do tipo. Na prática, isso funciona muito bem na minha experiência e você não perde os benefícios da segurança de tipo no estágio final (tempo de execução).
Martin Berger
@ Cody é possível ter metaprogramação no OCaml também com Extension Points , certo?
incorreto 14/09/16
@ Ignus Acho que não sei muito sobre Pontos de Extensão, embora eu o refira no link para o blog da Jane Street.
Cody
11
Meu compilador C é escrito em C, mas isso não significa que você pode manipular o AST em C ... #
22460 BlueRaja #
2
@immibis: Obviamente, mas se é isso o que ele quis dizer, essa afirmação é vaga e não tem relação com a pergunta ...
BlueRaja - Danny Pflughoeft 15/16
1

Como exemplo, considere F # (baseado em OCaml). O F # não é totalmente homoicônico, mas suporta a obtenção do código de uma função como AST sob certas circunstâncias.

Em F #, você printseria representado como um Exprimpresso como:

Let (message, Value ("Hello world"), Call (None, print_endline, [message]))

Para destacar melhor a estrutura, aqui está uma maneira alternativa de como você pode criar a mesma Expr:

let messageVar = Var("message", typeof<string>)
let expr = Expr.Let(messageVar,
                    Expr.Value("Hello world"),
                    Expr.Call(print_endline_method, [Expr.Var(messageVar)]))
svick
fonte
Eu não entendi isso. Você quer dizer que o F # permite "construir" o AST de uma expressão e depois executá-lo? Em caso afirmativo, qual é a diferença com os idiomas que permitem usar a eval(<string>)função? ( De acordo com muitos recursos, tendo função eval é diferente de ter homoiconicity - é a razão pela qual você disse F # não é totalmente homoiconic?)
incud
@ Ignus Você pode criar o AST por conta própria ou deixar o compilador fazer isso. A homoiconicidade "permite que todo código na linguagem seja acessado e transformado como dados" . No F #, você pode acessar algum código como dados. (Por exemplo, você precisa marcar printcom o [<ReflectedDefinition>]atributo.)
svick