【问题标题】:Extending a datatype in Haskell在 Haskell 中扩展数据类型
【发布时间】:2011-10-16 21:52:34
【问题描述】:

Haskell 新手在这里。

我为最小的类汇编语言编写了一个评估器。

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

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

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

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

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

【问题讨论】:

    标签: haskell types extend


    【解决方案1】:

    这个问题被 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
    

    例如

    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
    

    例如

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

    有关此方法的更多信息,以及关于表达问题的一般信息,请查看 Ralf Laemmel 在 Channel 9 上的视频12

    但是,正如 cmets 中所注意到的,此解决方案改变了语义。例如,表达式列表不再合法:

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

    在功能珍珠"Data types a la carte" 中介绍了使用类型签名的联积的更通用的解决方案。另请参阅纸上的 Wadler 的comment

    【讨论】:

    • 哇,这是一个非常好的视角!谢谢!
    • 这改变了程序的含义,即 [Expr] 与 Expr a 不一样 => [a]
    • @monadic:太晚了。一开始我没听懂你在说什么,所以继续胡思乱想。当需要评估指令列表时,它没有进行类型检查,您的评论立即对我有意义。我回到第一方了吗?我只记得使用了 Either 的列表,但这根本不实用!无论如何,感谢您的预期洞察力。我只是没能按时理解。
    • @Seymour Kooze 使用存在类型。 data ExprE = forall a. Expr a => a 然后 [ExprE] 表现出旧行为
    • @monadic:我尝试了你的最后一个建议,但最终我需要为 ExprE 指定一个构造函数。特别是,除非我将其写为data ExprE = forall a. Expr a => E a,否则我无法运行您的 sn-p。可以避免吗,就像你的例子一样?
    【解决方案2】:

    您可以使用 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。

    【讨论】:

    • 实际上 (+/-) 解决了 monadic 在使用 Federico 的策略时注意到的问题。谢谢!
    • 需要注意的一点是,由于您对Exp 唯一能做的就是eval,所以您不妨直接处理Int。通常,可以用(部分)应用于相关值的可用操作的记录来替换存在。
    • @hammar 我不会说“一般”。例如,对于Num 的存在,你会怎么做?
    • @augustss:啊,当然。当类型类包含返回相同类型的函数时,它不起作用,因为您可以从类型类对其执行附加操作的任意组合。只要所有类函数都采用存在类型的单个参数并返回一些非存在类型,它就可以工作。谢谢指正。
    【解决方案3】:

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

    【讨论】:

    • 我明白了。在那种情况下,我会咬紧牙关,重组我目前所做的工作。感谢您的洞察力。
    • 语法通常用抽象语法树表示。完全可以根据“脱糖”AST(前者作为后者的扩展)来定义“糖化”AST。如果我只想添加一种新的字符串文字,我应该能够在 AST 中保留一种 new 类型的字符串文字(不是翻译版本!)而无需大量复制或更改我的数据结构.我想要它,例如在糖化语法方面产生语义错误。
    【解决方案4】:

    一个(更简单的)选项是向您的 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)
    

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

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

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

    Desugar :: Expr Extended -> Expr Core
    

    您可能还对论文“Trees that grow”中描述的更复杂的方法感兴趣。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-10
      • 1970-01-01
      • 2012-09-06
      • 2023-03-11
      • 2010-12-13
      • 1970-01-01
      • 2020-10-19
      • 1970-01-01
      相关资源
      最近更新 更多