ADT 是一种您可以通过其他方式检查和操作的形式,而不仅仅是评估它。一旦你在一个函数调用中隐藏了所有有趣的数据,你就无法对它做任何事情,只能对其进行评估。考虑这个定义,类似于您问题中的定义,但使用 Var 术语来表示变量,并删除了 Mul 和 Neg 术语以专注于加法。
data Expr a = Constant a
| Add (Expr a) (Expr a)
| Var String
deriving Show
当然,要写的函数是eval。它需要一种查找变量值的方法,而且很简单:
-- 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
但假设您还没有变量映射。您有一个大型表达式,您将针对不同的变量选择多次评估。一些愚蠢的递归函数构建了如下表达式:
Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Add (Constant 1)
(Var "x"))))))
每次评估时都重新计算1+1+1+1+1+1 会很浪费:如果您的评估者能够意识到这只是Add (Constant 6) (Var "x") 的另一种写作方式,那不是很好吗?
因此,您编写了一个表达式优化器,它会在任何变量可用之前运行并尝试简化表达式。当然,您可以应用许多简化规则;下面我只实现了两个非常简单的例子来说明这一点。
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
现在我们愚蠢的表情看起来如何?
> 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")
所有不必要的东西都被删除了,你现在有一个很好的干净的表达式来尝试x的各种值。
你如何在函数中使用这个表达式的表示来做同样的事情?你不能,因为在表达式的初始规范和它的最终评估之间没有“中间形式”:你只能将表达式视为一个单一的、不透明的函数调用。以特定值 x 对其进行评估必然会重新评估每个子表达式,并且无法将它们分开。
这是您在问题中提出的函数类型的扩展,再次丰富了变量:
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
用同样的傻表达式计算多次:
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"))))))
它按预期工作:
> sample $ \_var -> 5
11
但每次你尝试不同的x 时,它都必须做一堆加法,即使加法和变量大多不相关。而且您无法简化表达式树。你不能在定义它时简化它:也就是说,你不能让add 更聪明,因为它根本无法检查它的参数:它的参数是函数,就add 而言,它可以做任何事。而且你也不能在构造它之后简化它:此时你只有一个不透明的函数,它接受一个变量查找函数并产生一个值。
通过将问题的重要部分单独建模为数据类型,您可以使它们成为您的程序可以智能操作的值。如果您将它们保留为函数,您会得到一个更短但功能更弱的程序,因为您将所有信息锁定在只有 GHC 可以操作的 lambdas 中。
一旦您使用 ADT 编写了它,如果您愿意,不难将该表示法折叠回更短的基于函数的表示法。也就是说,有一个类型的函数可能会很好
convert :: Expr a -> FExpr a
但事实上,我们已经这样做了!这正是eval 的类型。由于 FExpr 类型别名,您可能没有注意到,它没有在 eval 的定义中使用。
因此在某种程度上,ADT 表示更通用、更强大,就像一棵可以以多种不同方式折叠起来的树。其中一种方法是评估它,就像基于函数的表示一样。但还有其他的:
- 在计算之前简化表达式
- 生成一个所有变量的列表,这些变量必须为该表达式的格式正确定义
- 计算树最深部分的嵌套深度,以估计评估者可能需要多少堆栈帧
- 将表达式转换为近似于 Haskell 表达式的字符串,您可以键入以获得相同的结果
因此,如果可能的话,您希望尽可能长时间地使用信息丰富的 ADT,然后在您有特定的事情与它相关时最终将树折叠成更紧凑的形式。