Pergunta

Deus, odeio o termo "cheiro de código", mas não consigo pensar em nada mais preciso.

Estou projetando uma linguagem e um compilador de alto nível para Espaço em branco no meu tempo livre para aprender sobre construção de compiladores, design de linguagem e programação funcional (o compilador está sendo escrito em Haskell).

Durante a fase de geração de código do compilador, tenho que manter dados de "estado" enquanto percorro a árvore de sintaxe.Por exemplo, ao compilar instruções de controle de fluxo, preciso gerar nomes exclusivos para os rótulos saltarem (rótulos gerados a partir de um contador que é passado, atualizado e retornado, e o valor antigo do contador nunca deve ser usado novamente).Outro exemplo é quando encontro literais de string em linha na árvore de sintaxe, eles precisam ser convertidos permanentemente em variáveis ​​de heap (no espaço em branco, as strings são melhor armazenadas no heap).Atualmente estou agrupando todo o módulo de geração de código na mônada de estado para lidar com isso.

Disseram-me que escrever um compilador é um problema adequado ao paradigma funcional, mas acho que estou projetando isso da mesma maneira que projetaria em C (você realmente pode escrever C em qualquer linguagem - até mesmo Haskell com mônadas de estado).

Quero aprender como pensar em Haskell (ou melhor, no paradigma funcional) - não em C com sintaxe Haskell.Devo realmente tentar eliminar/minimizar o uso da mônada de estado ou é um "padrão de design" funcional legítimo?

Foi útil?

Solução

Eu diria que esse estado em geral não é um cheiro de código, desde que seja mantido pequeno e bem controlado.

Isso significa que usar mônadas como State, ST ou customizadas, ou apenas ter uma estrutura de dados contendo dados de estado que você passa para alguns lugares, não é uma coisa ruim.(Na verdade, mônadas são apenas uma ajuda para fazer exatamente isso!) No entanto, ter um estado que se espalha por todos os lados (sim, isso significa você, mônada IO!) É um mau cheiro.

Um exemplo bastante claro disso foi quando minha equipe estava trabalhando em nossa inscrição para o Concurso de Programação ICFP 2009 (o código está disponível em git://git.cynic.net/haskell/icfp-contest-2009).Acabamos com várias peças modulares diferentes para isso:

  • VM:a máquina virtual que executou o programa de simulação
  • Controladores:vários conjuntos diferentes de rotinas que leem a saída do simulador e geram novas entradas de controle
  • Solução:geração do arquivo de solução com base na saída dos controladores
  • Visualizadores:vários conjuntos diferentes de rotinas que leem as portas de entrada e saída e geram algum tipo de visualização ou registro do que estava acontecendo à medida que a simulação progredia

Cada um deles tem seu próprio estado e todos interagem de diversas maneiras por meio dos valores de entrada e saída da VM.Tínhamos vários controladores e visualizadores diferentes, cada um com seu próprio tipo de estado.

O ponto principal aqui era que os componentes internos de qualquer estado específico eram limitados aos seus próprios módulos específicos, e cada módulo não sabia nada sobre a existência de estado para outros módulos.Qualquer conjunto específico de código e dados com estado geralmente tinha apenas algumas dezenas de linhas, com alguns itens de dados no estado.

Tudo isso foi reunido em uma pequena função de cerca de uma dúzia de linhas que não tinha acesso ao interior de nenhum dos estados, e que apenas chamava as coisas certas na ordem correta enquanto percorria a simulação e passava por um período muito limitado. quantidade de informações externas para cada módulo (juntamente com o estado anterior do módulo, é claro).

Quando o estado é usado de maneira tão limitada e o sistema de tipos impede que você o modifique inadvertidamente, é muito fácil de manusear.É uma das belezas de Haskell permitir que você faça isso.

Uma resposta diz: "Não use Mônadas". Do meu ponto de vista, isso é exatamente para trás.Mônadas são uma estrutura de controle que, entre outras coisas, pode ajudar a minimizar a quantidade de código que afeta o estado.Se você olhar os analisadores monádicos como exemplo, o estado da análise (ou seja, o texto que está sendo analisado, o quão longe alguém chegou nele, quaisquer avisos acumulados, etc.) deve passar por todos os combinadores usados ​​no analisador .No entanto, haverá apenas alguns combinadores que realmente manipularão o Estado diretamente;qualquer outra coisa usa uma dessas poucas funções.Isso permite que você veja claramente e em um só lugar toda uma pequena quantidade de código que pode alterar o estado e raciocine mais facilmente sobre como ele pode ser alterado, novamente tornando mais fácil lidar com isso.

Outras dicas

Escrevi vários compiladores em Haskell, e uma mônada de estado é uma solução razoável para muitos problemas de compilador.Mas você quer mantê-lo abstrato – não deixe óbvio que está usando uma mônada.

Aqui está um exemplo do compilador Glasgow Haskell (que eu fiz não escrever;Eu apenas trabalho em torno de algumas arestas), onde construímos gráficos de fluxo de controle.Aqui estão as maneiras básicas de fazer gráficos:

empyGraph    :: Graph
mkLabel      :: Label -> Graph
mkAssignment :: Assignment -> Graph  -- modify a register or memory
mkTransfer   :: ControlTransfer -> Graph   -- any control transfer
(<*>)        :: Graph -> Graph -> Graph

Mas, como você descobriu, manter um estoque de rótulos exclusivos é, na melhor das hipóteses, entediante, por isso também oferecemos estas funções:

withFreshLabel :: (Label -> Graph) -> Graph
mkIfThenElse :: (Label -> Label -> Graph) -- branch condition
             -> Graph   -- code in the 'then' branch
             -> Graph   -- code in the 'else' branch 
             -> Graph   -- resulting if-then-else construct

O todo Graph thing é um tipo abstrato, e o tradutor simplesmente constrói gráficos de maneira puramente funcional, sem estar ciente de que algo monádico está acontecendo.Então, quando o gráfico é finalmente construído, para transformá-lo em um tipo de dados algébrico a partir do qual podemos gerar código, fornecemos a ele um suprimento de rótulos exclusivos, executamos a mônada de estado e extraímos a estrutura de dados.

A mônada estatal está escondida embaixo;embora não seja exposto ao cliente, a definição de Graph é algo assim:

type Graph = RealGraph -> [Label] -> (RealGraph, [Label])

ou um pouco mais precisamente

type Graph = RealGraph -> State [Label] RealGraph
  -- a Graph is a monadic function from a successor RealGraph to a new RealGraph

Com a mônada estatal escondida atrás de uma camada de abstração, não é nada fedorento!

Você já olhou Atribuir gramáticas (AG)?(Mais informações em Wikipédia e um artigo no Leitor Mônada)?

Com AG você pode adicionar atributos para uma árvore de sintaxe.Esses atributos são separados em sintetizado e herdado atributos.

Atributos sintetizados são coisas que você gera (ou sintetiza) a partir de sua árvore de sintaxe, pode ser o código gerado, ou todos os comentários, ou qualquer outra coisa de seu interesse.

Os atributos herdados são inseridos em sua árvore de sintaxe, podendo ser o ambiente ou uma lista de rótulos para usar durante a geração do código.

Na Universidade de Utrecht usamos o Sistema de gramática de atributos (UUAGC) para escrever compiladores.Este é um pré-processador que gera código haskell (.hs arquivos) do fornecido .ag arquivos.


Porém, se você ainda está aprendendo Haskell, talvez este não seja o momento de começar a aprender mais uma camada de abstração sobre isso.

Nesse caso, você poderia escrever manualmente o tipo de código que as gramáticas de atributos geram para você, por exemplo:

data AbstractSyntax = Literal Int | Block AbstractSyntax
                    | Comment String AbstractSyntax

compile :: AbstractSyntax -> [Label] -> (Code, Comments)
compile (Literal x) _      = (generateCode x, [])
compile (Block ast) (l:ls) = let (code', comments) = compile ast ls
                             in (labelCode l code', comments)
compile (Comment s ast) ls = let (code, comments') = compile ast ls
                             in (code, s : comments')

generateCode :: Int -> Code
labelCode :: Label -> Code -> Code

É possível que você queira um functor aplicativo em vez de uma mônada:

http://www.haskell.org/haskellwiki/Applicative_functor

Acho que o artigo original explica melhor que o wiki:

http://www.soi.city.ac.uk/~ross/papers/Applicative.html

Não acho que usar o State Monad seja um cheiro de código quando usado para modelar o estado.

Se você precisar encaixar suas funções, poderá fazer isso explicitamente, tomando o estado como um argumento e devolvendo -o em cada função.A State Monad oferece uma boa abstração:Ele passa o estado junto para você e fornece muita função útil para combinar funções que exigem estado.Nesse caso, usar o State Monad (ou Applicatives) não é um cheiro de código.

No entanto, se você usar a Mônada de Estado para imitar um estilo imperativo de programação, enquanto uma solução funcional seria suficiente, você está apenas complicando as coisas.

Em geral você deve tentar evitar o estado sempre que possível, mas isso nem sempre é prático. Applicative faz com que o código eficaz pareça mais agradável e funcional, especialmente o código de passagem de árvore pode se beneficiar desse estilo.Para o problema de geração de nomes existe agora um pacote bastante interessante disponível: oferta de valor.

Bem, não use mônadas.O poder da programação funcional é a pureza das funções e sua reutilização.Tem um artigo que um professor meu escreveu uma vez e ele é um dos caras que ajudou a construir Haskell.

O artigo se chama "Por que a programação funcional é importante", sugiro que você leia.É uma boa leitura.

vamos ter cuidado com a terminologia aqui.O Estado não é mau por si só;linguagens funcionais têm estado.O que é um "cheiro de código" é quando você deseja atribuir valores a variáveis ​​​​e alterá-los.

É claro que a mônada de estado Haskell existe exatamente por esse motivo - assim como acontece com E/S, ela permite que você faça coisas inseguras e não funcionais em um contexto restrito.

Então, sim, provavelmente é um cheiro de código.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top