Atualmente, estou trabalhando em um intérprete simples para uma linguagem de programação e tenho um tipo de dados como este:
data Expr
= Variable String
| Number Int
| Add [Expr]
| Sub Expr Expr
E eu tenho muitas funções que fazem coisas simples como:
-- Substitute a value for a variable
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue = go
where
go (Variable x)
| x == name = Number newValue
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
-- Replace subtraction with a constant with addition by a negative number
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd = go
where
go (Sub x (Number y)) =
Add [go x, Number (-y)]
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Mas em cada uma dessas funções, tenho que repetir a parte que chama o código recursivamente com apenas uma pequena alteração em uma parte da função. Existe alguma maneira de fazer isso de maneira mais genérica? Prefiro não ter que copiar e colar esta parte:
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
E apenas mude um único caso de cada vez, porque parece ineficiente duplicar código como este.
A única solução que eu poderia encontrar é ter uma função que chame uma função primeiro em toda a estrutura de dados e depois recursivamente no resultado da seguinte forma:
recurseAfter :: (Expr -> Expr) -> Expr -> Expr
recurseAfter f x =
case f x of
Add xs ->
Add $ map (recurseAfter f) xs
Sub x y ->
Sub (recurseAfter f x) (recurseAfter f y)
other -> other
substituteName :: String -> Int -> Expr -> Expr
substituteName name newValue =
recurseAfter $ \case
Variable x
| x == name -> Number newValue
other -> other
replaceSubWithAdd :: Expr -> Expr
replaceSubWithAdd =
recurseAfter $ \case
Sub x (Number y) ->
Add [x, Number (-y)]
other -> other
Mas sinto que provavelmente já deveria haver uma maneira mais simples de fazer isso. Estou esquecendo de algo?
Add :: Expr -> Expr -> Expr
vez deAdd :: [Expr] -> Expr
e livre-seSub
completamente.recurseAfter
estáana
disfarçado. Você pode querer olhar para anamorfismos erecursion-schemes
. Dito isto, acho que sua solução final é a mais curta possível. Mudar para osrecursion-schemes
anamorfismos oficiais não vai economizar muito.Respostas:
Parabéns, você acabou de descobrir os anamorfismos!
Aqui está o seu código, reformulado para que funcione com o
recursion-schemes
pacote. Infelizmente, não é mais curto, pois precisamos de um padrão para fazer as máquinas funcionarem. (Pode haver alguma maneira automagica de evitar o clichê, por exemplo, usando genéricos. Eu simplesmente não sei.)Abaixo, o seu
recurseAfter
é substituído pelo padrãoana
.Primeiro, definimos seu tipo recursivo, bem como o functor no qual é o ponto fixo.
Em seguida, conectamos os dois com algumas instâncias para que possamos nos desdobrar
Expr
no isomórficoExprF Expr
e dobrá-lo para trás.Por fim, adaptamos seu código original e adicionamos alguns testes.
Uma alternativa poderia ser definir
ExprF a
apenas e derivartype Expr = Fix ExprF
. Isso economiza parte do clichê acima (por exemplo, as duas instâncias), ao custo de ter que usar emFix (VariableF ...)
vez deVariable ...
, bem como o análogo para os outros construtores.Pode-se aliviar ainda mais o uso de sinônimos de padrão (ao custo de um pouco mais de clichê).
Atualização: Finalmente encontrei a ferramenta automagic, usando o modelo Haskell. Isso torna o código inteiro razoavelmente curto. Observe que o
ExprF
functor e as duas instâncias acima ainda existem sob o capô, e ainda precisamos usá-los. Só economizamos o trabalho de defini-los manualmente, mas isso economiza muito esforço.fonte
Expr
explicitamente, e não algo assimtype Expr = Fix ExprF
?Fix
+ o construtor real. Usar a última abordagem com a automação TH é melhor, IMO.Como uma abordagem alternativa, este também é um caso de uso típico para o
uniplate
pacote. Ele pode usarData.Data
genéricos em vez do Template Haskell para gerar o padrão, portanto, se você derData
exemplos para o seuExpr
:a
transform
função fromData.Generics.Uniplate.Data
aplica uma função recursivamente a cada aninhadoExpr
:Observe que,
replaceSubWithAdd
em particular, a funçãof
é escrita para executar uma substituição não recursiva;transform
torna a entrada recursivax :: Expr
, fazendo a mesma mágica para a função auxiliarana
que na resposta do @ chi:Isso não é mais curto que a solução Template Haskell da @ chi. Uma vantagem potencial é que
uniplate
fornece algumas funções adicionais que podem ser úteis. Por exemplo, se você usardescend
no lugar detransform
, ele transforma apenas os filhos imediatos, que podem lhe dar controle sobre onde a recursão ocorre ou você pode usarrewrite
para refazer a transformação do resultado das transformações até atingir um ponto fixo. Uma desvantagem potencial é que o "anamorfismo" parece muito mais legal do que o "uniplate".Programa completo:
fonte