如果我错了,请纠正我,但似乎Haskell中的代数数据类型在许多你将在OO语言中使用类和继承的情况下都很有用。但是有一个很大的区别:一旦声明了代数数据类型,它就无法在其他地方扩展。它是“封闭的”。在OO中,您可以扩展已定义的类。例如:
data Maybe a = Nothing | Just a
我无法在不修改此声明的情况下以某种方式为此类型添加其他选项。那么这个系统有什么好处呢?似乎OO方式可以更具扩展性。
答案 0 :(得分:80)
答案与代码易于扩展的方式有关,这是Phil Wadler称之为“表达问题”的类和代数数据类型之间的张力:
使用代数数据类型
在事物上添加新的操作非常便宜:您只需定义一个新功能。这些事情的所有旧功能继续保持不变。
添加新的类的东西非常昂贵:你必须添加一个新的构造函数和现有的数据类型,你必须编辑并重新编译使用该类型的每个函数。
使用课程
添加新的种类非常便宜:只需添加一个新的子类,并根据需要在该类中定义专门的方法,适用于所有现有业务。超类和所有其他子类继续保持不变。
在事物上添加新的操作非常昂贵:您必须向超类添加新的方法声明并且可能将方法定义添加到每个现有的子类。在实践中,负担取决于方法。
因此,代数数据类型是封闭的,因为封闭类型很好地支持某些类型的程序演化。例如,如果您的数据类型定义了一种语言,则很容易添加新的编译器过程而不会使旧的无效或更改数据。
可以使用“开放”数据类型,但除了在严格控制的情况下,类型检查变得困难。 Todd Millstein在支持开放代数类型和可扩展函数的语言设计上做了一些very beautiful work ,所有这些都使用模块化类型检查器。我发现他的报纸很高兴阅读。
答案 1 :(得分:67)
ADT关闭的事实使得编写总函数变得容易多了。对于其类型的所有可能值,这些函数总是产生结果,例如
maybeToList :: Maybe a -> [a]
maybeToList Nothing = []
maybeToList (Just x) = [x]
如果Maybe
已打开,有人可能会添加额外的构造函数,而maybeToList
函数会突然中断。
在OO中,当你使用继承来扩展类型时,这不是问题,因为当你调用一个没有特定重载的函数时,它只能使用超类的实现。也就是说,如果printPerson(Person p)
是Student
的子类,则可以使用Student
对象调用Person
。
在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的一部分。
答案 2 :(得分:15)
如果你写一个像
这样的函数maybeToList Nothing = []
maybeToList (Just x) = [x]
然后你知道它永远不会产生运行时错误,因为你已经涵盖了所有的情况。只要Maybe类型是可扩展的,这就不再是真的。在需要可扩展类型的情况下(并且它们比您想象的要少),规范的Haskell解决方案是使用类型类。
答案 3 :(得分:11)
选中“打开数据类型并打开功能”http://lambda-the-ultimate.org/node/1453
在面向对象的语言中,它是 通过定义新的方便扩展数据 类,但很难添加 新功能。在功能上 语言,情况正好相反: 添加新功能不会 问题,但扩展数据(添加 新数据构造函数)需要 修改现有代码。问题 支持两个方向 可扩展性被称为 表达问题。我们提出开放 数据类型和开放功能 表达式的轻量级解决方案 Haskell语言中的问题。该 想法是开放数据的构造者 类型和开放函数的方程 可以分散在整个地方 程序。特别是,他们可能会 驻留在不同的模块中。该 预期的语义如下: 程序应该表现得像数据一样 类型和功能已关闭, 在一个地方定义。的顺序 函数方程由下式确定 最合适的模式匹配,其中a 特定模式优先于 一个非特定的。我们表明我们的 解决方案适用于 表达问题,通用 编程和例外。我们画草图 两个实现。一个简单的, 源自语义,一个 基于相互递归的模块 允许单独编译。
答案 4 :(得分:7)
首先,作为查理答案的对照,这不是函数式编程所固有的。 OCaml具有open unions or polymorphic variants的概念,它基本上可以满足您的需求。
至于为什么,我相信这个选择是针对Haskell的,因为
因此,如果您更喜欢data Color r b g = Red r | Blue b | Green g
类型,那么它很容易制作,并且您可以轻松地将其设置为monad或functor,或者其他功能需要。
答案 5 :(得分:6)
关于这个(不可否认的老问题)的一些优秀答案,但我觉得我必须投入几美分。
我无法在不修改此声明的情况下以某种方式为此类型添加其他选项。那么这个系统有什么好处呢?似乎OO方式可以更具扩展性。
我相信,对此的答案是,开放式总和给你的那种可扩展性 not 总是一个加号,相应地,OO 迫使>你这是一个弱点。
封闭联盟的优势在于其详尽无遗:如果您在编译时修复了所有替代方案,那么您可以确定不存在您的代码无法处理的无法预料的情况。这是许多问题域中的有价值的属性,例如,在语言的抽象语法树中。如果你正在编写一个编译器,那么该语言的表达式属于预定义的,封闭的子集 - 你不希望人们能够在运行时添加新的子类,而编译器不会理解!
事实上,编译器AST是访问者模式的经典四人组激励示例之一,它是封闭总和和详尽模式匹配的OOP对应物。反思OO程序员最终发明了一种恢复已结算金额的模式这一事实是有益的。
同样,程序和功能程序员发明了模式来获得总和的效果。最简单的是“函数记录”编码,它对应于OO接口。功能记录实际上是调度表。 (请注意,C程序员已经使用这种技术多年了!)诀窍在于,通常存在大量可能的给定类型的函数 - 通常无限多。因此,如果您有一个记录类型,其字段是函数,那么这可以轻松地支持一组天文大或无限的替代方案。而且,由于记录是在运行时创建的,并且可以根据运行时条件灵活地完成,因此替代方案是后期绑定。
我最后的评论是,在我看来,OO已经让太多人相信可扩展性与后期绑定同义(例如,为类型添加新子类的能力)在运行时),当这通常不是真的。后期绑定是一种技术的可扩展性。另一种技术是组合 - 从构建块的固定词汇表和用于将它们组装在一起的规则构建复杂对象。词汇和规则理想上很小,但设计的目的是让它们具有丰富的交互,使你能够构建非常复杂的东西。
功能性编程 - 特别是ML / Haskell静态类型的风味 - 长期以来强调了后期绑定的组合。但实际上,这两种技术都存在于这两种范式中,应该是一个优秀程序员的工具包。
值得注意的是,编程语言本身就是组合的基本例子。编程语言具有有限的,希望简单的语法,允许您组合其元素以编写任何可能的程序。 (这实际上可以追溯到上面的编译器/访客模式示例并激励它。)
答案 6 :(得分:2)
另一种(或多或少)直观的查看数据类型和类型类与面向对象类的方法如下:
OO语言中的类 Foo 既代表具体类型 Foo ,也代表所有 Foo -types的类:那些直接或间接来自 Foo 。
在OO语言中,你恰巧对 Foo -types类进行隐式编程,它允许你“扩展” Foo 。
答案 7 :(得分:1)
好的,在这里“开放”你的意思是“可以衍生自”而不是Ruby和Smalltalk意义上的开放,你可以在运行时使用新方法扩展一个类,对吗?
在任何情况下,请注意两件事:首先,在大多数主要基于继承的OO语言中,有一种方法可以声明一个类来限制它的继承能力。 Java有“最终”,而且在C ++中存在这样的问题。因此,它只是作为其他OO语言的默认选项。
其次,您可以仍然创建一个使用已关闭的ADT并添加其他方法或不同实现的新类型。所以你不是那么受限制的。再次,他们似乎正式拥有相同的力量;你能在一个人中表达的东西可以用另一个来表达。
真实的是,函数式编程确实是一种不同的范式(“模式”)。如果你期望它应该像OO语言,你会经常感到惊讶。