Je travaille actuellement sur un interpréteur simple pour un langage de programmation et j'ai un type de données comme celui-ci:
data Expr
= Variable String
| Number Int
| Add [Expr]
| Sub Expr Expr
Et j'ai de nombreuses fonctions qui font des choses simples comme:
-- 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
Mais dans chacune de ces fonctions, je dois répéter la partie qui appelle le code récursivement avec juste un petit changement à une partie de la fonction. Existe-t-il un moyen existant de procéder de manière plus générique? Je préfère ne pas avoir à copier et coller cette partie:
go (Add xs) =
Add $ map go xs
go (Sub x y) =
Sub (go x) (go y)
go other = other
Et il suffit de changer un seul cas à chaque fois car il semble inefficace de dupliquer du code comme celui-ci.
La seule solution que je pourrais trouver est d'avoir une fonction qui appelle d'abord une fonction sur toute la structure de données, puis récursivement sur le résultat comme ceci:
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
Mais je pense qu'il devrait probablement déjà y avoir un moyen plus simple de le faire. Suis-je en train de manquer quelque chose?
Add :: Expr -> Expr -> Expr
au lieu deAdd :: [Expr] -> Expr
et supprimez-lesSub
complètement.recurseAfter
est -ana
déguisé. Vous voudrez peut-être regarder les anamorphismes etrecursion-schemes
. Cela étant dit, je pense que votre solution finale est aussi courte que possible. Passer auxrecursion-schemes
anamorphismes officiels ne fera pas beaucoup d'économies.Réponses:
Félicitations, vous venez de redécouvrir les anamorphismes!
Voici votre code, reformulé pour qu'il fonctionne avec le
recursion-schemes
package. Hélas, ce n'est pas plus court, car nous avons besoin d'un passe-partout pour faire fonctionner les machines. (Il pourrait y avoir un moyen automagique d'éviter le passe-partout, par exemple en utilisant des génériques. Je ne sais tout simplement pas.)Ci-dessous, votre
recurseAfter
est remplacé par le standardana
.Nous définissons d'abord votre type récursif, ainsi que le foncteur dont il est le point fixe.
Ensuite, nous connectons les deux avec quelques instances afin de pouvoir nous déplier
Expr
dans l'isomorpheExprF Expr
et le replier.Enfin, nous adaptons votre code d'origine et ajoutons quelques tests.
Une alternative pourrait être de définir
ExprF a
uniquement, puis de dérivertype Expr = Fix ExprF
. Cela permet d'économiser une partie du passe-partout ci-dessus (par exemple les deux instances), au prix d'avoir à utiliser à laFix (VariableF ...)
place deVariable ...
, ainsi que l'analogue pour les autres constructeurs.On pourrait encore atténuer cela en utilisant des synonymes de modèle (au prix d'un peu plus de passe-partout, cependant).
Mise à jour: j'ai enfin trouvé l'outil automagique, en utilisant le modèle Haskell. Cela rend l'ensemble du code raisonnablement court. Notez que le
ExprF
foncteur et les deux instances ci-dessus existent toujours sous le capot, et nous devons encore les utiliser. Nous économisons seulement les tracas d'avoir à les définir manuellement, mais cela à lui seul économise beaucoup d'efforts.la source
Expr
explicitement, plutôt que quelque chose commetype Expr = Fix ExprF
?Fix
+ le vrai constructeur. L'utilisation de la dernière approche avec l'automatisation TH est plus agréable, OMI.En tant qu'approche alternative, il s'agit également d'un cas d'utilisation typique pour le
uniplate
package. Il peut utiliser desData.Data
génériques plutôt que Template Haskell pour générer le passe-partout, donc si vous dérivez desData
instances pour votreExpr
:alors la
transform
fonction deData.Generics.Uniplate.Data
applique une fonction récursivement à chaque imbriquéExpr
:Notez qu'en
replaceSubWithAdd
particulier, la fonctionf
est écrite pour effectuer une substitution non récursive;transform
le rend récursifx :: Expr
, il fait donc la même magie à la fonction d'assistance queana
dans la réponse de @ chi:Ce n'est pas plus court que la solution Template Haskell de @ chi. Un avantage potentiel est qu'il
uniplate
fournit des fonctions supplémentaires qui peuvent être utiles. Par exemple, si vous utilisezdescend
à la place detransform
, il transforme uniquement les enfants immédiats, ce qui peut vous donner le contrôle de l'endroit où la récursivité se produit, ou vous pouvez utiliserrewrite
pour re-transformer le résultat des transformations jusqu'à ce que vous atteigniez un point fixe. Un inconvénient potentiel est que "l'anamorphisme" semble beaucoup plus frais que "uniplate".Programme complet:
la source