为什么Haskell代数数据类型"封闭"?

Zif*_*fre 57 oop haskell types type-systems functional-programming

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

data Maybe a = Nothing | Just a
Run Code Online (Sandbox Code Playgroud)

以后我无法以某种方式为此类型添加另一个选项而不修改此声明.那么这个系统有什么好处呢?似乎OO方式可以更加可扩展.

Nor*_*sey 80

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

  • 使用代数数据类型,

    • 在事物上添加新操作非常便宜:您只需定义一个新功能.这些事情的所有旧功能继续不变.

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

  • 有课程,

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

    • 在事物上添加新操作非常昂贵:您必须向超类添加新方法声明,并可能向每个现有子类添加方法定义.在实践中,负担取决于方法.

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

可以具有"开放"数据类型,但除了在精心控制的情况下,类型检查变得困难.Todd Millstein在支持开放式代数类型和可扩展函数的语言设计方面做了一些非常漂亮的工作,所有这些都使用模块化类型检查器.我发现他的论文非常愉快.


Tom*_*rst 67

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

maybeToList :: Maybe a -> [a]
maybeToList Nothing  = []
maybeToList (Just x) = [x]
Run Code Online (Sandbox Code Playgroud)

如果Maybe是开放的,有人可以添加一个额外的构造函数,该maybeToList功能会突然中断.

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

在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
Run Code Online (Sandbox Code Playgroud)

现在,该==函数是完全打开的,您可以通过使其成为Eq类的实例来添加自己的类型.


请注意,已经有关于可扩展数据类型的想法的工作,但这绝对不是Haskell的一部分.


Dav*_*ave 15

如果你写一个像这样的函数

maybeToList Nothing = []
maybeToList (Just x) = [x]
Run Code Online (Sandbox Code Playgroud)

然后你知道它永远不会产生运行时错误,因为你已经涵盖了所有的情况.只要Maybe类型是可扩展的,这就不再是真的.在需要可扩展类型的情况下(并且它们比您想象的要少),规范的Haskell解决方案是使用类型类.


Don*_*art 11

检查"打开数据类型和打开函数" http://lambda-the-ultimate.org/node/1453

在面向对象语言中,通过定义新类很容易扩展数据,但很难添加新函数.在函数式语言中,情况正好相反:添加新函数不会带来任何问题,但扩展数据(添加新数据构造函数)需要修改现有代码.支持两个可扩展方向的问题被称为表达问题.我们将开放数据类型和开放函数作为Haskell语言中表达式问题的轻量级解决方案.这个想法是开放数据类型的构造函数和开放函数的方程可以分散在整个程序中.特别是,它们可以驻留在不同的模块中.预期的语义如下:程序应该像在一个地方定义的数据类型和函数一样.函数方程的顺序由最佳拟合模式匹配确定,其中特定模式优先于非特定模式.我们表明我们的解决方案适用于表达式问题,泛型编程和异常.我们描绘了两个实现.一个简单的,源自语义,一个基于相互递归的模块,允许单独编译.


ram*_*ion 7

首先,作为查理答案的对应点,这不是函数式编程所固有的.OCaml具有开放式联合或多态变体的概念,它基本上可以满足您的需求.

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

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

因此,如果你想要一个data Color r b g = Red r | Blue b | Green g类型,它很容易制作,你可以轻松地使它像一个monad或functor或其他函数需要.

  • 那个颜色的例子看起来很怪异.这不是红色,绿色或蓝色吗?您不想要"产品"类型,而不是"总和"类型吗? (3认同)
  • 您还可以创建一个类并将现有类型声明为它的实例,因此您也可以通过这种方式扩展它. (2认同)

Lui*_*las 6

关于这个(不可否认的老问题)的一些优秀答案,但我觉得我必须投入我的几美分.

以后我无法以某种方式为此类型添加另一个选项而不修改此声明.那么这个系统有什么好处呢?似乎OO方式可以更加可扩展.

我相信,对此的答案是,开放金额给你的那种可扩展性并不总是一个加分,相应地,OO 强迫你这样做的事实是一个弱点.

封闭联盟的优势在于它们的详尽性:如果您在编译时修复了所有备选方案,那么您可以确定不存在代码无法处理的无法预料的情况.这是许多问题域中的有价值的属性,例如,在语言的抽象语法树中.如果您正在编写编译器,那么该语言的表达式将属于预定义的,封闭的子集 - 您希望人们能够在运行时添加新的子类,而编译器无法理解!

实际上,编译器AST是访问者模式的经典四人组激励示例之一,它是封闭总和和详尽模式匹配的OOP对应物.反思一个事实是,OO程序员最终发明了一种恢复封闭金额的模式.

同样,程序和功能程序员发明了模式来获得总和的效果.最简单的是"函数记录"编码,它对应于OO接口.功能记录实际上是一个调度表.(请注意,C程序员已经使用这种技术多年了!)诀窍在于,通常存在大量可能的给定类型的函数 - 通常无限多.因此,如果你有一个记录类型,其字段是函数,那么这可以很容易地支持一个天文大或无限的替代方案.而且,由于记录是在运行时创建的,并且可以根据运行时条件灵活地完成,因此备选方案是后期绑定的.

我最后的评论是,在我看来,OO已经让太多人相信可扩展性与后期绑定同义(例如,在运行时向类型添加新子类的能力),当这不是一般都是.后期绑定是一种可扩展性技术.另一种技术是组合 - 从构建块的固定词汇表和用于将它们组装在一起的规则构建复杂对象.词汇和规则理想上很小,但设计的目的是让它们具有丰富的交互,允许您构建非常复杂的事物.

功能性编程 - 特别是ML/Haskell静态类型的风味 - 长期以来强调了后期绑定的组合.但实际上,这两种技术都存在于两种范式中,并且应该存在于优秀程序员的工具箱中.

值得注意的是,编程语言本身就是组合的基本例子.编程语言具有有限的,希望简单的语法,允许您组合其元素以编写任何可能的程序.(这实际上可以追溯到上面的编译器/访客模式示例并激励它.)