Uma opinião divergente: a homoiconicidade de Lisp é muito menos útil do que você imagina.
Para entender macros sintáticas, é importante entender os compiladores. O trabalho de um compilador é transformar código legível por humanos em código executável. De uma perspectiva de alto nível, isso tem duas fases gerais: análise e geração de código .
A análise é o processo de ler o código, interpretá-lo de acordo com um conjunto de regras formais e transformá-lo em uma estrutura em árvore, geralmente conhecida como AST (Abstract Syntax Tree). Por toda a diversidade entre linguagens de programação, essa é uma notável semelhança: essencialmente toda linguagem de programação de uso geral analisa uma estrutura em árvore.
A geração de código toma o AST do analisador como entrada e o transforma em código executável por meio da aplicação de regras formais. Da perspectiva do desempenho, essa é uma tarefa muito mais simples; muitos compiladores de idiomas de alto nível gastam 75% ou mais de seu tempo analisando.
É importante lembrar que o Lisp é muito, muito antigo. Entre as linguagens de programação, apenas o FORTRAN é mais antigo que o Lisp. No passado, a análise (a parte lenta da compilação) era considerada uma arte sombria e misteriosa. Os trabalhos originais de John McCarthy sobre a teoria de Lisp (quando era apenas uma idéia que ele nunca pensou que pudesse realmente ser implementada como uma linguagem de programação de computador real) descrevem uma sintaxe um pouco mais complexa e expressiva do que as modernas "expressões S em todos os lugares para tudo" "notação. Isso aconteceu mais tarde, quando as pessoas estavam tentando implementá-lo. Como a análise não era bem compreendida na época, eles basicamente analisaram e transformaram a sintaxe em uma estrutura de árvore homoicônica, a fim de tornar o trabalho do analisador absolutamente trivial. O resultado final é que você (o desenvolvedor) precisa fazer muito do analisador ' s trabalha para isso escrevendo o AST formal diretamente no seu código. A homoiconicidade não "facilita tanto as macros" quanto torna mais difícil escrever todo o resto!
O problema disso é que, especialmente com a digitação dinâmica, é muito difícil para as expressões S levarem muitas informações semânticas com elas. Quando toda a sua sintaxe é do mesmo tipo (listas de listas), não há muito no contexto fornecido pela sintaxe e, portanto, o sistema macro tem muito pouco com o que trabalhar.
A teoria dos compiladores percorreu um longo caminho desde a década de 1960, quando o Lisp foi inventado e, embora as coisas que ele realizou fossem impressionantes para a época, elas parecem bastante primitivas agora. Para um exemplo de um sistema moderno de metaprogramação, dê uma olhada na linguagem Boo (infelizmente subestimada). O Boo é de tipo estatístico, orientado a objeto e de código aberto, portanto, todo nó AST possui um tipo com uma estrutura bem definida na qual um desenvolvedor de macros pode ler o código. A linguagem possui uma sintaxe relativamente simples, inspirada no Python, com várias palavras-chave que dão significado semântico intrínseco às estruturas em árvore construídas a partir delas, e sua metaprogramação possui uma sintaxe de quasiquote intuitiva para simplificar a criação de novos nós AST.
Aqui está uma macro que eu criei ontem quando percebi que estava aplicando o mesmo padrão a vários lugares diferentes no código da GUI, onde eu chamava BeginUpdate()
um controle de interface do usuário, realizava uma atualização em um try
bloco e depois chamava EndUpdate()
:
macro UIUpdate(value as Expression):
return [|
$value.BeginUpdate()
try:
$(UIUpdate.Body)
ensure:
$value.EndUpdate()
|]
O macro
comando é, de fato, uma macro em si , que recebe um corpo da macro como entrada e gera uma classe para processar a macro. Ele usa o nome da macro como uma variável que representa o MacroStatement
nó AST que representa a chamada da macro. O [| ... |] é um bloco de quasiquotas, gerando o AST que corresponde ao código dentro e dentro do bloco de quasiquotes, o símbolo $ fornece o recurso "sem aspas", substituindo em um nó conforme especificado.
Com isso, é possível escrever:
UIUpdate myComboBox:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
e expanda para:
myComboBox.BeginUpdate()
try:
LoadDataInto(myComboBox)
myComboBox.SelectedIndex = 0
ensure:
myComboBox.EndUpdate()
Expressar a macro dessa maneira é mais simples e intuitivo do que em uma macro Lisp, porque o desenvolvedor conhece a estrutura MacroStatement
e sabe como as propriedades Arguments
e Body
funcionam, e esse conhecimento inerente pode ser usado para expressar os conceitos envolvidos de uma maneira muito intuitiva. maneira. Também é mais seguro, porque o compilador conhece a estrutura MacroStatement
e, se você tentar codificar algo que não é válido para a MacroStatement
, o compilador o capturará imediatamente e relatará o erro, em vez de você não saber até que algo exploda em você. tempo de execução.
Enxertar macros em Haskell, Python, Java, Scala etc. não é difícil, porque essas linguagens não são homoicônicas; é difícil porque os idiomas não foram projetados para eles e funciona melhor quando a hierarquia AST do seu idioma é projetada desde o início para ser examinada e manipulada por um sistema macro. Quando você trabalha com uma linguagem que foi projetada com metaprogramação desde o início, as macros são muito mais simples e fáceis de trabalhar!