【问题标题】:Why are Haskell algebraic data types "closed"?为什么 Haskell 代数数据类型是“封闭的”?
【发布时间】:2010-10-26 15:14:55
【问题描述】:

如果我错了,请纠正我,但 Haskell 中的代数数据类型似乎在您将在 OO 语言中使用类和继承的许多情况下很有用。但是有一个很大的区别:一旦声明了代数数据类型,就不能在其他地方进行扩展。它是“关闭的”。在 OO 中,您可以扩展已定义的类。例如:

data Maybe a = Nothing | Just a

如果不修改此声明,我以后无法以某种方式向此类型添加另一个选项。那么这个系统有什么好处呢?看起来OO方式的可扩展性要好得多。

【问题讨论】:

  • 您应该将 Haskell 的数据类型视为结构和枚举的增强版本(在 C 意义上,不确定其他语言如何使用它们)。它们只是愚蠢的数据。真正的 OOP 意义上的对象和类具有相当多的控制和业务逻辑,在 Haskell 中,您将使用高阶函数、函数记录、类型类等等。
  • 我想知道这是不是一个很好的类比:用 Java 术语重新表述 Haskell(选择一种流行的 OOP 语言),数据类型就像最终类,类型类就像 JDK8 接口(包括默认方法,它使接口非常接近于直接多重继承!))。构造函数支持组合。因此,通过这个类比,您可以看到,通过“放弃”只是实现继承,Haskell 确实“失去”了传统 OOP 中的任何力量——如果有的话,它只是切断了一个有点危险且容易被替换的工具更安全的替代品?
  • 我相信您可以通过创建一个包含旧数据类型作为一个选项的新数据类型来扩展它们

标签: oop haskell types functional-programming type-systems


【解决方案1】:

好的,这里的“开放”是指“可以派生自”,而不是 Ruby 和 Smalltalk 意义上的开放,您可以在运行时使用新方法扩展类,对吧?

无论如何,请注意两件事:首先,在大多数主要基于继承的 OO 语言中,有一种方法可以声明一个类以限制其被继承的能力。 Java 有“final”,在 C++ 中有针对此的 hack。因此,它只是将其他 OO 语言作为默认选项。

其次,您可以仍然创建一个使用封闭 ADT 并添加其他方法或不同实现的新类型。所以你并没有真正受到这种方式的限制。同样,它们似乎在形式上具有相同的强度;你可以用一种表达的东西可以用另一种表达。

事实上,函数式编程确实是一种不同的范式(“模式”)。如果您期望它应该像一门面向对象的语言,那么您会经常感到惊讶。

【讨论】:

  • 不,他的意思是“Ruby”意义上的开放——以后可以添加新的数据类型变体。
【解决方案2】:

如果你写一个类似的函数

maybeToList Nothing = []
maybeToList (Just x) = [x]

那么您就知道它永远不会产生运行时错误,因为您已经涵盖了所有情况。一旦 Maybe 类型是可扩展的,这就不再成立了。在您需要可扩展类型的情况下(它们比您想象的要少),规范的 Haskell 解决方案是使用类型类。

【讨论】:

  • Maybe 类型不是一个很好的例子,但我经常发现我想扩展该类型。
【解决方案3】:

ADT 是封闭的这一事实使得编写总函数变得容易得多。这些函数总是针对其类型的所有可能值产生结果,例如。

maybeToList :: Maybe a -> [a]
maybeToList Nothing  = []
maybeToList (Just x) = [x]

如果Maybe 是打开的,有人可以添加一个额外的构造函数,maybeToList 函数会突然中断。

在 OO 中,这不是问题,当您使用继承来扩展类型时,因为当您调用没有特定重载的函数时,它可以只使用超类的实现。即,如果StudentPerson 的子类,您可以使用Student 对象调用printPerson(Person p)

在 Haskell 中,当您需要扩展类型时,通常会使用封装和类型类。例如:

class Eq a where
   (==) :: a -> a -> Bool

instance Eq Bool where
  False == False = True
  False == True  = False
  True  == False = False
  True  == True  = True

instance Eq a => Eq [a] where
  []     == []     = True
  (x:xs) == (y:ys) = x == y && xs == ys
  _      == _      = False

现在,== 函数完全开放,您可以通过将其设为Eq 类的实例来添加自己的类型。


请注意,extensible datatypes 的想法已经有了工作,但这绝对不是 Haskell 的一部分。

【讨论】:

  • 谢谢,现在这更有意义了!
【解决方案4】:

首先,与查理的回答相反,这不是函数式编程所固有的。 OCaml 有open unions or polymorphic variants 的概念,基本上可以做你想做的事。

至于为什么,我相信这个选择是为Haskell做出的,因为

  • 这让类型是可预测的 - 它们只是每个构造函数的有限数量
  • 定义自己的类型很容易。
  • 许多 Haskell 函数是多态的,类允许您扩展自定义类型以适应函数参数(想想 Java 的接口)。

因此,如果您更愿意使用 data Color r b g = Red r | Blue b | Green g 类型,那么它很容易制作,并且您可以轻松地使其像 monad 或 functor 或其他所需的函数一样工作。

【讨论】:

  • 那个颜色的例子看起来很奇怪。那不会使它变成红色,绿色或蓝色吗?您不想要“产品”类型,而不是“总和”类型吗?
  • 我会给你一个奇怪的例子。也许Something a b c = Nada | JustA a | CoupleOf b c 会更好。
  • 您还可以创建一个类并将现有类型声明为它的实例,因此您也可以这样扩展。
【解决方案5】:

勾选“开放数据类型和开放函数”http://lambda-the-ultimate.org/node/1453

在面向对象的语言中,它是 通过定义新的来轻松扩展数据 类,但很难添加 新功能。在功能 语言,情况正好相反: 添加新功能不会 问题,但扩展数据(添加 新的数据构造函数)需要 修改现有代码。问题 支持两个方向的 可扩展性被称为 表达问题。我们提出开放 数据类型和开放功能作为 表达式的轻量级解决方案 Haskell 语言中的问题。这 想法是开放数据的构造函数 开函数的类型和方程 可以分散在整个 程序。特别是,他们可能 驻留在不同的模块中。这 预期的语义如下: 程序应该表现得好像数据 类型和功能已关闭, 在一处定义。的顺序 函数方程由下式确定 最佳匹配模式匹配,其中 特定模式优先于 一个不具体的。我们表明我们的 解决方案适用于 表达问题,泛型 编程和异常。我们素描 两个实现。一个简单的, 从语义派生,一个 基于相互递归的模块 允许单独编译。

【讨论】:

  • 我要添加对此的引用。我很高兴看到其他人提到它!
  • 这听起来有点狂野/难以分析。
【解决方案6】:

答案与代码易于扩展的方式、类和代数数据类型之间的紧张关系有关,Phil Wadler 称之为“表达式问题”:

  • 对于代数数据类型,

    • 在事物上添加一个新的操作非常便宜:你只需定义一个新函数。这些东西上的所有旧功能继续保持不变。

    • 添加一个新的种类的东西非常昂贵:你必须添加一个新的构造函数和现有的数据类型,你必须编辑并重新编译每个使用该类型的函数

  • 类,

    • 添加新的种类非常便宜:只需添加一个新的子类,并根据需要在该类中定义专门的方法,对于所有现有的操作。超类和所有其他子类继续保持不变。

    • 在事物上添加一个新的操作非常昂贵:你必须向超类添加一个新的方法声明并可能为每个现有的子类添加一个方法定义。在实践中,负担因方法而异。

因此,代数数据类型是封闭的,因为封闭类型很好地支持某些类型的程序演化。例如,如果您的数据类型定义了一种语言,则很容易添加新的编译器通道,而不会使旧的编译器通道失效或更改数据。

可能有“开放”数据类型,但除非在仔细控制的情况下,类型检查变得困难。 Todd Millstein 在支持开放代数类型和可扩展函数的语言设计上做了一些very beautiful work,所有这些都带有模块化类型检查器。我发现他的论文读起来非常愉快。

【讨论】:

    【解决方案7】:

    查看数据类型和类型类与面向对象类的另一种(或多或少)直观的方法如下:

    面向对象语言中的类 Foo 既代表具体类型 Foo 也代表所有 Foo 类型的类:那些直接或间接派生自 Foo

    在 OO 语言中,您只是碰巧针对 Foo 类型的类进行隐式编程,这允许您“扩展”Foo

    【讨论】:

      【解决方案8】:

      关于这个(诚然老的)问题的一些很好的答案,但我觉得我必须投入我的几分钱。

      如果不修改此声明,我以后无法以某种方式向此类型添加另一个选项。那么这个系统有什么好处呢?看起来OO方式的可扩展性要好得多。

      我相信,这个问题的答案是,开和给你的那种可扩展性并不总是加分,相应地,OO 强制 em> 这对你来说是一个弱点。

      封闭联合的优势在于它们的详尽性:如果您在编译时修复了所有备选方案,那么您可以确定不会出现您的代码无法处理的意外情况。这在许多问题领域中都是有价值的属性,例如,在语言的抽象语法树中。如果您正在编写一个编译器,该语言的表达式属于预定义的、封闭的子案例集——您确实希望人们能够在运行时添加您的编译器不支持的新子案例明白!

      事实上,编译器 AST 是访问者模式的经典四人组激励示例之一,它是封闭求和和穷举模式匹配的 OOP 对应物。反思一下 OO 程序员最终发明了一种模式来恢复封闭总和这一事实是有启发性的。

      同样,过程和函数式程序员已经发明了模式来获得求和的效果。最简单的是“函数记录”编码,对应OO接口。函数记录实际上是一个调度表。 (请注意,C 程序员多年来一直在使用这种技术!)诀窍在于,给定类型的可能函数通常有很多——通常是无限多的。因此,如果您有一个字段是函数的记录类型,那么它可以轻松支持天文数字般的大或无限的替代方案集。更重要的是,由于记录是在运行时创建的,并且可以根据运行时条件灵活地完成,因此替代方案是后期绑定

      我要说的最后一点是,在我看来,OO 让太多人相信可扩展性与 后期绑定 是同义词(例如,向类型添加新子案例的能力在运行时),当这通常不是真的时。后期绑定是一种可扩展性技术。另一种技术是组合——从固定的构建块词汇和将它们组合在一起的规则构建复杂的对象。理想情况下,词汇和规则很小,但经过精心设计,它们具有丰富的交互作用,可让您构建非常复杂的事物。

      函数式编程——尤其是 ML/Haskell 静态类型风格——长期以来一直强调组合而不是后期绑定。但实际上,这两种技术都存在于两种范式中,并且应该包含在优秀程序员的工具包中。

      还值得注意的是,编程语言本身就是组合的基本示例。一种编程语言有一个有限的、希望是简单的语法,它允许你组合它的元素来编写任何可能的程序。 (这实际上可以追溯到上面的编译器/访问者模式示例并激发它。)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-12-12
        • 2012-03-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多