L'ADT se présente sous une forme que vous pouvez inspecter et manipuler autrement qu'en l'évaluant simplement. Une fois que vous avez caché toutes les données intéressantes dans un appel de fonction, vous ne pouvez plus rien faire d'autre que de l'évaluer. Considérez cette définition, similaire à celle de votre question, mais avec un terme Var pour représenter les variables et avec les termes Mul et Neg supprimés pour se concentrer sur l'addition.
data Expr a = Constant a
| Add (Expr a) (Expr a)
| Var String
deriving Show
La fonction évidente à écrire est eval
bien sûr. Il faut un moyen de rechercher la valeur d'une variable, et c'est simple :
-- cheating a little bit by assuming all Vars are defined
eval :: Num a => Expr a -> (String -> a) -> a
eval (Constant x) _env = x
eval (Add x y) env = eval x env + eval y env
eval (Var x) env = env x
Mais supposons que vous n'ayez pas encore de mappage de variable. Vous avez une grande expression que vous allez évaluer plusieurs fois, pour différents choix de variables. Et une fonction récursive idiote a construit une expression comme :
Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Var "x"))))))
Il serait inutile de recalculer 1+1+1+1+1+1
à chaque fois que vous évaluez ceci : ne serait-ce pas bien si votre évaluateur pouvait se rendre compte que c'est juste une autre façon d'écrire Add (Constant 6) (Var "x")
?
Vous écrivez donc un optimiseur d'expression, qui s'exécute avant que les variables ne soient disponibles et tente de simplifier les expressions. Il existe de nombreuses règles de simplification que vous pouvez appliquer, bien sûr ; ci-dessous, je n'en ai implémenté que deux très simples pour illustrer mon propos.
simplify :: Num a => Expr a -> Expr a
simplify (Add (Constant x) (Constant y)) = Constant $ x + y
simplify (Add (Constant x) (Add (Constant y) z)) = simplify $ Add (Constant $ x + y) z
simplify x = x
Maintenant, à quoi ressemble notre expression idiote ?
> simplify $ Add (Constant 1) (Add (Constant 1) (Add (Constant 1) (Add (Constant 1) (Add (Constant 1) (Add (Constant 1) (Var "x"))))))
Add (Constant 6) (Var "x")
Toutes les choses inutiles ont été supprimées et vous avez maintenant une expression propre et agréable à utiliser pour différentes valeurs de x
.
Comment faire la même chose avec une représentation de cette expression en fonctions ? C'est impossible, car il n'existe pas de "forme intermédiaire" entre la spécification initiale de l'expression et son évaluation finale : on ne peut considérer l'expression que comme un appel de fonction unique et opaque. En l'évaluant à une valeur particulière de x
évalue nécessairement chacune des sous-expressions à nouveau, et il n'y a aucun moyen de les démêler.
Voici une extension du type fonctionnel que vous proposez dans votre question, à nouveau enrichie de variables :
type FExpr a = (String -> a) -> a
lit :: a -> FExpr a
lit x _env = x
add :: Num a => FExpr a -> FExpr a -> FExpr a
add x y env = x env + y env
var :: String -> FExpr a
var x env = env x
avec la même expression idiote à évaluer plusieurs fois :
sample :: Num a => FExpr a
sample = add (lit 1)
(add (lit 1)
(add (lit 1)
(add (lit 1)
(add (lit 1)
(add (lit 1)
(var "x"))))))
Il fonctionne comme prévu :
> sample $ \_var -> 5
11
Mais il doit faire un tas d'additions à chaque fois que vous l'essayez pour une autre x
même si l'addition et la variable sont pour la plupart sans rapport. Et il n'y a aucun moyen de simplifier l'arbre d'expression. On ne peut pas le simplifier en le définissant : c'est-à-dire qu'on ne peut pas faire de add
plus intelligente, parce qu'elle ne peut pas du tout inspecter ses arguments : ses arguments sont des fonctions qui, pour autant que l'on sache, ne sont pas des fonctions. add
est concerné, pourrait faire n'importe quoi. Et vous ne pouvez pas non plus la simplifier après l'avoir construite : à ce stade, vous avez juste une fonction opaque qui prend une fonction de recherche de variable et produit une valeur.
En modélisant les parties importantes de votre problème comme des types de données à part entière, vous en faites des valeurs que votre programme peut manipuler intelligemment. Si vous les laissez en tant que fonctions, vous obtenez un programme plus court mais moins puissant, car vous enfermez toutes les informations dans des lambdas que seul GHC peut manipuler.
Et une fois que vous l'avez écrit avec des ADT, il n'est pas difficile de ramener cette représentation à la représentation plus courte basée sur les fonctions si vous le souhaitez. C'est-à-dire qu'il peut être intéressant d'avoir une fonction de type
convert :: Expr a -> FExpr a
Mais en fait, nous l'avons déjà fait ! C'est exactement le type que eval
a. Vous ne l'avez peut-être pas remarqué à cause de l'alias de type FExpr, qui n'est pas utilisé dans la définition de eval
.
Ainsi, d'une certaine manière, la représentation ADT est plus générale et plus puissante, agissant comme un arbre que vous pouvez plier de nombreuses façons différentes. L'une de ces façons consiste à l'évaluer, exactement comme le fait la représentation basée sur les fonctions. Mais il y en a d'autres :
- Simplifier l'expression avant de l'évaluer
- Produire une liste de toutes les variables qui doivent être définies pour que cette expression soit bien formée
- Comptez la profondeur de l'imbrication de la partie la plus profonde de l'arbre, afin d'estimer le nombre de cadres de pile dont l'évaluateur pourrait avoir besoin.
- Convertir l'expression en une chaîne de caractères se rapprochant d'une expression Haskell que vous pourriez taper pour obtenir le même résultat.
Dans la mesure du possible, il est donc préférable de travailler avec des ADT riches en informations aussi longtemps que possible, puis de replier l'arbre dans une forme plus compacte lorsque vous avez quelque chose de spécifique à faire avec.