这是一个关于功能语言中静态类型系统的软问题,如ML系列。我理解为什么你需要数据类型来描述像列表和树这样的数据结构,但是像数据类型中的命题逻辑那样定义“表达式”似乎带来了一些便利并且不是必需的。例如
datatype arithmetic_exp = Constant of int
| Neg of arithmetic_exp
| Add of (arithmetic_exp * arithmetic_exp)
| Mult of (arithmetic_exp * arithmetic_exp)
定义了一组值,您可以在其上编写一个eval
函数,该函数将为您提供结果。你也可以定义4个函数:const: int -> int
,neg: int -> int
,add: int * int -> int
和mult: int * int -> int
然后排序add (mult (const 3, neg 2), neg 4)
的表达式会给你同样的东西没有任何静态安全性的损失。唯一的复杂因素是你必须做四件事而不是两件事。在学习SML和Haskell时,我一直在考虑哪些功能为您提供了必要的东西,哪些只是一种便利,所以这就是我要问的原因。我想如果你想要从值本身分离一个值的过程,这就很重要,但我不确定哪个有用。
非常感谢。
答案 0 :(得分:7)
基于初始/一阶/数据类型的编码(又名深度嵌入)和最终/高阶/基于评估器的编码(又名浅嵌入)之间存在二元性。实际上,您通常可以使用类型组合器而不是数据类型(并在两者之间来回转换)。
这是一个显示两种方法的模块:
{-# LANGUAGE GADTs, Rank2Types #-}
module Expr where
data Expr where
Val :: Int -> Expr
Add :: Expr -> Expr -> Expr
class Expr' a where
val :: Int -> a
add :: a -> a -> a
您可以看到这两个定义看起来非常相似。 Expr' a
基本上是在Expr
上描述代数,这意味着如果您有a
,则可以从Expr
中获得Expr' a
。同样,因为您可以编写实例Expr' Expr
,所以您可以将forall a. Expr' a => a
类型的术语重新定义为Expr
类型的语法值:
expr :: Expr' a => Expr -> a
expr e = case e of
Val n -> val n
Add p q -> add (expr p) (expr q)
instance Expr' Expr where
val = Val
add = Add
expr' :: (forall a. Expr' a => a) -> Expr
expr' e = e
最后,选择一个表示形式实际上取决于你的主要关注点:如果你想检查表达式的结构(例如,如果你想优化/编译它),如果你有权访问它就会更容易一个AST。另一方面,如果您只对使用折叠计算不变量(例如表达式的深度或其评估)感兴趣,则可以使用更高阶的编码。
答案 1 :(得分:4)
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表示更通用,更强大,可以作为一种树,你可以用许多不同的方式折叠。其中一种方法是以与基于函数的表示完全相同的方式对其进行评估。但还有其他人:
因此,如果可能的话,您希望尽可能长时间地使用信息丰富的ADT,然后一旦您有特定的事情,最终将树折叠成更紧凑的形式。