关于类型:在 Haskell 中扩展数据类型

Extending a datatype in Haskell

Haskell 新手在这里。

我为一种最小的类汇编语言编写了一个求值器。

现在,我想扩展该语言以支持一些语法糖,然后我将编译回仅使用原始运算符。想法是我不想再次触摸评估器模块。

我认为,在面向对象的处理方式中,可以扩展原始模块以支持语法糖运算符,在此处提供翻译规则。

除此之外,我只能考虑重写两个模块中的数据类型构造函数,以便它们不会发生名称冲突,然后从那里继续,就好像它们是完全不同的东西一样,但这意味着一些冗余,因为我会必须重复(仅使用其他名称)共同的运算符。同样,我认为这里的关键字是extend.

有没有一种功能性的方法来实现这一点?

感谢您抽出宝贵时间阅读此问题。


这个问题被 Phil Wadler 命名为"表达问题",用他的话来说:

The goal is to define a data type by cases, where one can add new cases to the data type and new functions over the data type, without recompiling existing code, and while retaining
static type safety.

拥有可扩展数据类型的一种解决方案是使用类型类。

作为一个例子,假设我们有一种简单的算术语言:

1
2
3
4
5
data Expr = Add Expr Expr | Mult Expr Expr | Const Int

run (Const x) = x
run (Add exp1 exp2)  = run exp1 + run exp2
run (Mult exp1 exp2) = run exp1 * run exp2

例如

1
2
ghci> run (Add (Mult (Const 1) (Const 3)) (Const 2))
5

如果我们想以可扩展的方式实现它,我们应该切换到类型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Expr a where
    run :: a -> Int


data Const = Const Int

instance Expr Const where
    run (Const x) = x


data Add a b = Add a b

instance (Expr a,Expr b) => Expr (Add a b) where
    run (Add expr1 expr2) = run expr1 + run expr2


data Mult a b = Mult a b

instance (Expr a, Expr b) => Expr (Mult a b) where
    run (Mult expr1 expr2) = run expr1 * run expr2

现在让我们扩展语言加减法:

1
2
3
4
data Sub a b = Sub a b

instance (Expr a, Expr b) => Expr (Sub a b) where
    run (Sub expr1 expr2) = run expr1 - run expr2

例如

1
2
ghci> run (Add (Sub (Const 1) (Const 4)) (Const 2))
-1

有关此方法的更多信息,以及关于表达问题的一般信息,请查看第 9 频道的 Ralf Laemmel 的视频 1 和 2。

但是,正如评论中所指出的,此解决方案改变了语义。例如,表达式列表不再合法:

1
[Add (Const 1) (Const 5), Const 6] -- does not typecheck

功能性珍珠"点菜数据类型"中介绍了使用类型签名的联积的更通用的解决方案。另请参阅 Wadler 对该论文的评论。


你可以使用存在类型做一些更像 OOP 的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
-- We need to enable the ExistentialQuantification extension.
{-# LANGUAGE ExistentialQuantification #-}

-- I want to use const as a term in the language, so let's hide Prelude.const.
import Prelude hiding (const)

-- First we need a type class to represent an expression we can evaluate
class Eval a where
  eval :: a -> Int

-- Then we create an existential type that represents every member of Eval
data Exp = forall t. Eval t => Exp t

-- We want to be able to evaluate all expressions, so make Exp a member of Eval.
-- Since the Exp type is just a wrapper around"any value that can be evaluated,"
-- we simply unwrap that value and call eval on it.
instance Eval Exp where
  eval (Exp e) = eval e

-- Then we define our base language; constants, addition and multiplication.
data BaseExp = Const Int | Add Exp Exp | Mul Exp Exp

-- We make sure we can evaluate the language by making it a member of Eval.
instance Eval BaseExp where
  eval (Const n) = n
  eval (Add a b) = eval a + eval b
  eval (Mul a b) = eval a * eval b

-- In order to avoid having to clutter our expressions with Exp everywhere,
-- let's define a few smart constructors.
add x y = Exp $ Add x y
mul x y = Exp $ Mul x y
const   = Exp . Const

-- However, now we want subtraction too, so we create another type for those
-- expressions.
data SubExp = Sub Exp Exp

-- Then we make sure that we know how to evaluate subtraction.
instance Eval SubExp where
  eval (Sub a b) = eval a - eval b

-- Finally, create a smart constructor for sub too.
sub x y = Exp $ Sub x y

通过这样做,我们实际上得到了一个可扩展类型,因此您可以例如在一个列表中混合扩展值和基值:

1
2
> map eval [sub (const 10) (const 3), add (const 1) (const 1)]
[7, 2]

然而,由于我们现在唯一能知道的关于 Exp 值的事情是它们在某种程度上是 Eval 的成员,所以我们不能进行模式匹配或做任何没有在类型类中指定的事情。在 OOP 术语中,将 Exp 一个 exp 值视为实现 Eval 接口的对象。如果你有一个 ISomethingThatCanBeEvaluated 类型的对象,显然你不能安全地将它转换成更具体的东西;这同样适用于 Exp.


语法糖通常由解析器处理;您将扩展(不是在 OO 继承的意义上)解析器以检测新结构并将它们转换为您的评估器可以处理的结构。


一个(更简单的)选项是向您的 AST 添加一个类型,以区分 Core 和 Extended:

1
2
3
4
5
6
7
8
data Core = Core
data Extended = Extended

data Expr t
  = Add (Expr t) (Expr t)
  | Mult (Expr t) (Expr t)
  | Const Int
  | Sugar t (Expr t) (Expr t)

一个表达式要么是核心的,要么是扩展的:编译器将确保它只包含相同类型的子表达式。

原始模块中的函数签名需要使用 Expr Core(而不仅仅是 Expr)

Desugar 函数将具有以下类型签名:

1
Desugar :: Expr Extended -> Expr Core

您可能还对论文"生长的树"中描述的更复杂的方法感兴趣。