为什么ML / Haskell数据类型对于定义像算术表达式这样的“语言”很有用?

时间:2017-09-05 10:15:25

标签: haskell types ml

这是一个关于功能语言中静态类型系统的软问题,如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 -> intneg: int -> intadd: int * int -> intmult: int * int -> int然后排序add (mult (const 3, neg 2), neg 4)的表达式会给你同样的东西没有任何静态安全性的损失。唯一的复杂因素是你必须做四件事而不是两件事。在学习SML和Haskell时,我一直在考虑哪些功能为您提供了必要的东西,哪些只是一种便利,所以这就是我要问的原因。我想如果你想要从值本身分离一个值的过程,这就很重要,但我不确定哪个有用。

非常感谢。

2 个答案:

答案 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表示更通用,更强大,可以作为一种树,你可以用许多不同的方式折叠。其中一种方法是以与基于函数的表示完全相同的方式对其进行评估。但还有其他人:

  • 在评估之前简化表达
  • 生成一个列表,列出必须为此表达式定义良好的所有变量
  • 计算树最深部分的嵌套程度,估计评估者可能需要多少堆栈帧
  • 将表达式转换为近似于您可以键入的Haskell表达式的String,以获得相同的结果

因此,如果可能的话,您希望尽可能长时间地使用信息丰富的ADT,然后一旦您有特定的事情,最终将树折叠成更紧凑的形式。