Haskell新手在这里。
我为一个最小的汇编语言写了一个评估器。
现在,我想扩展该语言以支持一些语法糖,然后我将编译回来仅使用原始运算符。意思是我不想再次触及评估模块。
在OO的做事方式中,我认为,可以扩展原始模块,以支持语法糖操作符,在此提供翻译规则。
除此之外,我只能考虑重写两个模块中的数据类型构造函数,以便它们不会发生名称冲突,并从那里继续,好像它们是完全不同的东西,但这意味着一些冗余,因为我会必须重复(只是与其他名称)共同的运营商。同样,我认为此处的关键字是扩展。
是否有实现这一目标的功能性方法?
感谢您抽出宝贵时间阅读此问题。
答案 0 :(得分:20)
这个问题被Phil Wadler用他的话语命名为“表达问题”:
目标是按案例定义数据类型,其中可以在数据类型上添加新案例,在数据类型上添加新函数,而无需重新编译现有代码,同时保留 静态类型安全。
具有可扩展数据类型的一种解决方案是使用类型类。
作为一个例子,我们假设我们有一个简单的算术语言:
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
e.g。
ghci> run (Add (Mult (Const 1) (Const 3)) (Const 2))
5
如果我们想以可扩展的方式实现它,我们应该切换到类型类:
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
现在让我们扩展语言添加减法:
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
e.g。
ghci> run (Add (Sub (Const 1) (Const 4)) (Const 2))
-1
有关此方法的更多信息,以及有关表达式问题的更多信息,请在第9频道查看Ralf Laemmel的视频1和2。
但是,正如评论中所注意到的,此解决方案会更改语义。例如,表达式列表不再合法:
[Add (Const 1) (Const 5), Const 6] -- does not typecheck
使用类型签名的副产品的更一般的解决方案在功能性珍珠"Data types a la carte"中呈现。另见纸上的Wadler comment。
答案 1 :(得分:6)
你可以使用existential types做一些类似OOP的事情:
-- 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
通过这样做,我们实际上获得了一个可扩展类型,因此您可以在列表中混合扩展值和基值:
> map eval [sub (const 10) (const 3), add (const 1) (const 1)]
[7, 2]
然而,由于我们现在唯一可以知道的关于Exp值的事情是它们不知何故是Eval的成员,我们不能模式匹配或做任何类型类中未指定的事情。在OOP术语中,将Exp exp值视为实现Eval接口的对象。如果你有一个ISomethingThatCanBeEvaluated类型的对象,显然你不能安全地把它变成更具体的东西;这同样适用于Exp。
答案 2 :(得分:3)
句法糖通常由解析器处理;您将扩展(不是在OO继承的意义上)解析器来检测新构造并将它们转换为您的求值程序可以处理的结构类型。
答案 3 :(得分:0)
一个(更简单的)选项是在AST中添加一个类型,以区分Core和Extended:
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)
表达式是Core或Extended:编译器将确保它只包含相同类型的子表达式。
原始模块中的功能签名需要使用Expr Core
(而不只是Expr
)
Desugar函数具有以下类型签名:
Desugar :: Expr Extended -> Expr Core
您可能也对文章“Trees that grow”中描述的更复杂的方法感兴趣。