Parece que a Template Haskell é frequentemente vista pela comunidade Haskell como uma conveniência lamentável. É difícil colocar em palavras exatamente o que observei a esse respeito, mas considere estes poucos exemplos
- O modelo Haskell listado em "O feio (mas necessário)" em resposta à pergunta Quais extensões do Haskell (GHC) os usuários devem usar / evitar?
- O modelo Haskell considerou uma solução temporária / inferior em Vetores sem caixa de texto do segmento de valores newtype'd (lista de discussão de bibliotecas)
- Yesod é frequentemente criticado por confiar demais no Template Haskell (veja a postagem do blog em resposta a esse sentimento)
Eu já vi várias postagens no blog em que as pessoas fazem coisas bem legais com o Template Haskell, permitindo uma sintaxe mais bonita que simplesmente não seria possível no Haskell comum, bem como uma tremenda redução de clichê. Então, por que o modelo Haskell é menosprezado dessa maneira? O que o torna indesejável? Em que circunstâncias o Template Haskell deve ser evitado e por quê?
haskell
template-haskell
Dan Burton
fonte
fonte
Respostas:
Uma razão para evitar Template Haskell é que, como um todo, não é totalmente seguro para o tipo, contrariando muito o "espírito de Haskell". Aqui estão alguns exemplos disso:
Exp
, mas não sabe se é uma expressão que representa a[Char]
ou a(a -> (forall b . b -> c))
ou o que for. O TH seria mais confiável se alguém pudesse expressar que uma função pode gerar apenas expressões de um determinado tipo, ou apenas declarações de função, ou apenas padrões de correspondência de construtor de dados, etc.foo
que não existe? Com sorte, você verá apenas isso quando realmente usar seu gerador de código, e somente nas circunstâncias que acionam a geração desse código específico. Também é muito difícil realizar testes unitários.O TH também é totalmente perigoso:
IO
, incluindo o lançamento de mísseis ou o roubo do seu cartão de crédito. Você não precisa procurar em todos os pacotes cabal que baixar em busca de explorações de TH.Existem alguns problemas que tornam as funções TH menos divertidas de usar como desenvolvedor de bibliotecas:
generateLenses [''Foo, ''Bar]
.forM_ [''Foo, ''Bar] generateLens
?Q
é apenas uma mônada, para que você possa usar todas as funções usuais. Algumas pessoas não sabem disso e, por causa disso, criam várias versões sobrecarregadas de essencialmente as mesmas funções com a mesma funcionalidade, e essas funções levam a um certo efeito de inchaço. Além disso, muitas pessoas escrevem seus geradores naQ
mônada, mesmo quando não precisam, o que é como escreverbla :: IO Int; bla = return 3
; você está dando a uma função mais "ambiente" do que precisa, e os clientes da função são obrigados a fornecer esse ambiente como efeito disso.Finalmente, existem algumas coisas que tornam as funções TH menos divertidas de usar como usuário final:
Q Dec
, ela pode gerar absolutamente qualquer coisa no nível superior de um módulo e você não tem absolutamente nenhum controle sobre o que será gerado.fonte
Esta é apenas a minha opinião.
É feio de usar.
$(fooBar ''Asdf)
simplesmente não parece bom. Superficial, claro, mas contribui.É ainda mais feio escrever. Às vezes, a citação funciona, mas na maioria das vezes você precisa fazer enxertos e encanamentos manuais em AST. A API é grande e pesada, há sempre muitos casos com os quais você não se importa, mas ainda precisa despachar, e os casos com os quais você se preocupa tendem a estar presentes em várias formas semelhantes, mas não idênticas (dados versus novo tipo, registro estilo vs. construtores normais, e assim por diante). É chato e repetitivo escrever e complicado o suficiente para não ser mecânico. A proposta de reforma aborda parte disso (tornando as cotações mais amplamente aplicáveis).
A restrição de palco é um inferno. Não conseguir dividir as funções definidas no mesmo módulo é a parte menor: a outra conseqüência é que, se você tiver uma emenda de nível superior, tudo o que estiver depois no módulo ficará fora do escopo para qualquer coisa anterior. Outras linguagens com essa propriedade (C, C ++) a tornam viável, permitindo que você encaminhe declarações, mas Haskell não. Se você precisar de referências cíclicas entre declarações emendadas ou suas dependências e dependentes, geralmente está ferrado.
É indisciplinado. O que quero dizer com isso é que, na maioria das vezes, quando você expressa uma abstração, existe algum tipo de princípio ou conceito por trás dessa abstração. Para muitas abstrações, o princípio por trás delas pode ser expresso em seus tipos. Para classes de tipo, você pode frequentemente formular leis que as instâncias devem obedecer e os clientes podem assumir. Se você usar o novo recurso genérico do GHC para abstrair a forma de uma declaração de instância sobre qualquer tipo de dados (dentro dos limites), você poderá dizer "para tipos de soma, funciona assim, para tipos de produtos, funciona assim". O modelo Haskell, por outro lado, são apenas macros. Não é abstração no nível das idéias, mas abstração no nível das ASTs, o que é melhor, mas apenas modestamente, do que a abstração no nível do texto sem formatação. *
Liga você ao GHC. Em teoria, outro compilador poderia implementá-lo, mas na prática duvido que isso aconteça. (Isso contrasta com várias extensões de sistema de tipo que, embora possam apenas ser implementadas pelo GHC no momento, eu poderia facilmente imaginar ser adotado por outros compiladores no futuro e eventualmente padronizado.)
A API não é estável. Quando novos recursos de idioma são adicionados ao GHC e o pacote template-haskell é atualizado para suportá-los, isso geralmente envolve alterações incompatíveis com os versões anteriores dos tipos de dados TH. Se você deseja que seu código TH seja compatível com mais de uma versão do GHC, você precisa ter muito cuidado e usar
CPP
.Há um princípio geral de que você deve usar a ferramenta certa para o trabalho e a menor que seja suficiente, e nessa analogia, o modelo Haskell é algo assim . Se existe uma maneira de fazer isso que não é o Template Haskell, geralmente é preferível.
A vantagem do Template Haskell é que você pode fazer coisas que não poderia fazer de outra maneira, e isso é importante. Na maioria das vezes, as coisas para as quais o TH é usado só poderiam ser feitas se elas fossem implementadas diretamente como recursos do compilador. O TH é extremamente benéfico para ter ambos, porque permite fazer essas coisas e porque você protótipo de extensões em potencial do compilador de uma maneira muito mais leve e reutilizável (veja os vários pacotes de lentes, por exemplo).
Para resumir por que acho que existem sentimentos negativos em relação ao Template Haskell: ele resolve muitos problemas, mas para qualquer problema resolvido, parece que deve haver uma solução melhor, mais elegante e disciplinada, mais adequada para resolver esse problema, um que não resolva o problema gerando automaticamente o padrão, mas removendo a necessidade de ter o padrão.
* Embora eu ache frequentemente que
CPP
tem uma melhor relação potência / peso para os problemas que ele pode resolver.EDIT 23-04-14: O que eu frequentemente tentava abordar acima, e apenas recentemente obtive exatamente, é que há uma distinção importante entre abstração e desduplicação. A abstração adequada geralmente resulta em desduplicação como efeito colateral, e a duplicação geralmente é um sinal revelador de abstração inadequada, mas não é por isso que é valiosa. Abstração adequada é o que torna o código correto, compreensível e sustentável. A desduplicação apenas a torna mais curta. O modelo Haskell, como macros em geral, é uma ferramenta para desduplicação.
fonte
Gostaria de abordar alguns dos pontos mencionados pelo dflemstr.
Não acho que você não possa verificar TH como algo preocupante. Por quê? Porque mesmo se houver um erro, ainda haverá tempo de compilação. Não tenho certeza se isso reforça meu argumento, mas isso é semelhante em espírito aos erros que você recebe ao usar modelos em C ++. Eu acho que esses erros são mais compreensíveis que os erros do C ++, pois você obterá uma versão impressa do código gerado.
Se uma expressão TH / quase-quoter faz algo tão avançado que cantos difíceis podem se esconder, então talvez seja desaconselhável?
Eu quebro essa regra bastante com quasi-quoters em que tenho trabalhado ultimamente (usando haskell-src-exts / meta) - https://github.com/mgsloan/quasi-extras/tree/master/examples . Eu sei que isso introduz alguns erros, como não poder emendar as compreensões de lista generalizadas. No entanto, acho que há uma boa chance de que algumas das idéias em http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal acabem no compilador. Até então, as bibliotecas para analisar Haskell em árvores TH são uma aproximação quase perfeita.
Em relação à velocidade / dependência da compilação, podemos usar o pacote "zeroth" para alinhar o código gerado. Isso é pelo menos agradável para os usuários de uma determinada biblioteca, mas não podemos fazer muito melhor no caso de editar a biblioteca. As dependências de TH podem inchar os binários gerados? Eu pensei que deixou de fora tudo o que não é referenciado pelo código compilado.
A restrição de preparação / divisão das etapas de compilação do módulo Haskell é péssima.
Opacidade do RE: é o mesmo para qualquer função de biblioteca que você chamar. Você não tem controle sobre o que Data.List.groupBy fará. Você apenas tem uma "garantia" / convenção razoável de que os números de versão informam sobre a compatibilidade. É uma questão de mudança diferente quando.
É aqui que o uso do zeroth compensa - você já está fazendo a versão dos arquivos gerados - para saber sempre quando a forma do código gerado foi alterada. Observar as diferenças pode ser um pouco complicado, porém, para grandes quantidades de código gerado, então esse é um lugar onde uma melhor interface de desenvolvedor seria útil.
Monolitismo do ER: Você certamente pode pós-processar os resultados de uma expressão TH, usando seu próprio código em tempo de compilação. Não seria muito código filtrar o tipo / nome da declaração de nível superior. Você pode imaginar escrever uma função que faça isso genericamente. Para modificar / desmonolitizar os quasiquoters, é possível padronizar a correspondência no "QuasiQuoter" e extrair as transformações usadas, ou fazer uma nova em termos dos antigos.
fonte
[Dec]
e remover coisas que não deseja, mas digamos que a função lê um arquivo de definição externa ao gerar a interface JSON. Só porque você não usa issoDec
não faz o gerador parar de procurar o arquivo de definição, fazendo com que a compilação falhe. Por esse motivo, seria bom ter uma versão mais restritiva daQ
mônada que permitisse gerar novos nomes (e coisas assim), mas não permitisseIO
, de modo que, como você diz, alguém pudesse filtrar seus resultados / fazer outras composições .Esta resposta é uma resposta às questões levantadas por illissius, ponto por ponto:
Concordo. Sinto que $ () foi escolhido para parecer que fazia parte do idioma - usando o familiar palete de símbolos de Haskell. No entanto, é exatamente isso que você não quer nos símbolos usados para a emenda de macro. Definitivamente se misturam demais, e esse aspecto cosmético é bastante importante. Gosto da aparência de emendas, porque elas são visualmente distintas.
Também concordo com isso, no entanto, como observam alguns dos comentários em "Novas direções para o TH", a falta de boas citações AST prontas para uso não é uma falha crítica. Neste pacote WIP, busco solucionar esses problemas no formato de biblioteca: https://github.com/mgsloan/quasi-extras . Até agora, permito a emenda em mais alguns lugares do que o habitual e posso combinar os padrões nos ASTs.
Já me deparei com a questão das definições cíclicas de TH antes ... É bastante irritante. Existe uma solução, mas é feia - envolva as coisas envolvidas na dependência cíclica em uma expressão TH que combina todas as declarações geradas. Um desses geradores de declarações poderia ser apenas um quasi-quoter que aceita o código Haskell.
Só é sem princípios se você fizer coisas sem princípios. A única diferença é que, com o compilador implementado mecanismos para abstração, você tem mais confiança de que a abstração não está vazando. Talvez democratizar o design da linguagem pareça um pouco assustador! Os criadores de bibliotecas de TH precisam documentar bem e definir claramente o significado e os resultados das ferramentas que eles fornecem. Um bom exemplo de TH com princípios é o pacote derivado: http://hackage.haskell.org/package/derive - ele usa uma DSL de forma que o exemplo de muitas das derivações / especifique / a derivação real.
Esse é um ponto muito bom - a API do TH é bastante grande e desajeitada. Reimplementar, parece que pode ser difícil. No entanto, existem realmente apenas algumas maneiras de reduzir o problema de representar ASTs da Haskell. Eu imagino que copiar os TH ADTs e gravar um conversor na representação AST interna o levaria a uma boa parte do caminho até lá. Isso seria equivalente ao esforço (não insignificante) de criar haskell-src-meta. Também poderia ser simplesmente reimplementado imprimindo bastante o TH AST e usando o analisador interno do compilador.
Embora eu possa estar errado, não vejo o TH como uma extensão de compilador tão complicada, do ponto de vista da implementação. Este é realmente um dos benefícios de "manter as coisas simples" e não ter a camada fundamental como um sistema de modelos teoricamente atraente e estaticamente verificável.
Este também é um bom argumento, mas um pouco dramático. Embora tenha havido adições à API recentemente, elas não foram extensivamente indutoras de quebras. Além disso, acho que, com a citação AST superior que mencionei anteriormente, a API que realmente precisa ser usada pode ser substancialmente reduzida. Se nenhuma construção / correspondência precisar de funções distintas e, em vez disso, for expressa como literais, a maior parte da API desaparecerá. Além disso, o código que você escreve seria portado mais facilmente para representações AST para idiomas semelhantes ao Haskell.
Em resumo, acho que o TH é uma ferramenta poderosa e semi-negligenciada. Menos ódio pode levar a um ecossistema de bibliotecas mais animado, incentivando a implementação de mais protótipos de recursos de linguagem. Tem sido observado que o TH é uma ferramenta sobrecarregada, que pode deixar você / fazer / quase qualquer coisa. Anarquia! Bem, é minha opinião que esse poder pode permitir que você supere a maioria de suas limitações e construa sistemas capazes de abordagens de meta-programação bastante baseadas em princípios. Vale a pena o uso de hacks feios para simular a implementação "adequada", pois dessa forma o design da implementação "adequada" ficará gradualmente claro.
Na minha versão ideal pessoal do nirvana, grande parte da linguagem sairia do compilador, para bibliotecas dessa variedade. O fato de os recursos serem implementados como bibliotecas não influencia fortemente sua capacidade de abstrair fielmente.
Qual é a resposta típica de Haskell ao código padrão? Abstração. Quais são as nossas abstrações favoritas? Funções e tipeclasses!
As classes de tipos permitem definir um conjunto de métodos que podem ser usados em todos os tipos de funções genéricas nessa classe. Entretanto, além disso, a única maneira de as classes ajudarem a evitar clichês é oferecendo "definições padrão". Agora, aqui está um exemplo de um recurso sem princípios!
Conjuntos mínimos de ligação não podem ser declarados / verificados pelo compilador. Isso pode levar a definições inadvertidas que resultam em fundo devido à recursão mútua.
Apesar da grande conveniência e poder que isso renderia, você não pode especificar padrões de superclasse, devido a instâncias órfãs http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ Isso permitiria corrigir o hierarquia numérica graciosamente!
A busca por recursos do tipo TH para padrões de métodos levou a http://www.haskell.org/haskellwiki/GHC.Generics . Embora isso seja interessante, minha única experiência em depurar códigos usando esses genéricos era quase impossível, devido ao tamanho do tipo induzido e ao ADT tão complicado quanto um AST. https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c
Em outras palavras, isso ocorreu após os recursos fornecidos pelo TH, mas foi necessário elevar um domínio inteiro da linguagem, a linguagem da construção, para uma representação do sistema de tipos. Embora eu possa vê-lo funcionando bem para o seu problema comum, para os complexos, parece propenso a produzir uma pilha de símbolos muito mais aterrorizante do que os hackers da TH.
O TH fornece a computação em tempo de compilação em nível de valor do código de saída, enquanto os genéricos obriga a elevar a parte de correspondência / recursão de padrão do código no sistema de tipos. Embora isso restrinja o usuário de algumas maneiras bastante úteis, não acho que a complexidade valha a pena.
Penso que a rejeição do TH e da metaprogramação do tipo lisp levaram à preferência por coisas como padrões de método, em vez de declarações de instâncias mais flexíveis e macroexpansíveis. A disciplina de evitar coisas que podem levar a resultados imprevisíveis é sábia, no entanto, não devemos ignorar que o sistema de tipos capazes de Haskell permite uma metaprogramação mais confiável do que em muitos outros ambientes (verificando o código gerado).
fonte
Um problema bastante pragmático com o Template Haskell é que ele só funciona quando o interpretador de bytecode do GHC está disponível, o que não é o caso em todas as arquiteturas. Portanto, se o seu programa usa o Template Haskell ou depende de bibliotecas que o utilizam, ele não será executado em máquinas com uma CPU ARM, MIPS, S390 ou PowerPC.
Isso é relevante na prática: o git-anexo é uma ferramenta escrita em Haskell que faz sentido executar em máquinas preocupadas com armazenamento, essas máquinas geralmente têm CPUs não i386. Pessoalmente, eu corro o git-anexo em um NSLU 2 (32 MB de RAM, CPU de 266 MHz; você sabia que o Haskell funciona bem nesse hardware?) Se ele usasse o Template Haskell, isso não é possível.
(A situação sobre o GHC no ARM está melhorando bastante atualmente e acho que 7.4.2 funciona mesmo, mas o ponto ainda permanece).
fonte
ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )'
)Por que o TH é ruim? Para mim, tudo se resume a isso:
Pense nisso. Metade do apelo de Haskell é que seu design de alto nível permite evitar grandes quantidades de código inútil que você precisa escrever em outros idiomas. Se você precisa de geração de código em tempo de compilação, está basicamente dizendo que o seu idioma ou o design do seu aplicativo falhou. E nós programadores não gostamos de falhar.
Às vezes, é claro, é necessário. Mas, às vezes, você pode evitar a necessidade de TH apenas sendo um pouco mais inteligente com seus projetos.
(A outra coisa é que o TH é de nível bastante baixo. Não há um design de alto nível; muitos detalhes de implementação interna do GHC estão expostos. E isso torna a API propensa a mudanças ...)
fonte