如果我说错了,请纠正我,但似乎在Haskell中,代数数据类型在许多情况下都可以替代面向对象语言中的类和继承。但有一个重大区别:一旦声明了代数数据类型,它就无法在其他地方扩展。它是“封闭”的。而在面向对象编程中,您可以扩展已定义的类。例如:
data Maybe a = Nothing | Just a
我没有办法在不修改这个声明的情况下添加另一个选项。那么这个系统有什么好处呢?似乎面向对象的方式更加可扩展。
如果我说错了,请纠正我,但似乎在Haskell中,代数数据类型在许多情况下都可以替代面向对象语言中的类和继承。但有一个重大区别:一旦声明了代数数据类型,它就无法在其他地方扩展。它是“封闭”的。而在面向对象编程中,您可以扩展已定义的类。例如:
data Maybe a = Nothing | Just a
我没有办法在不修改这个声明的情况下添加另一个选项。那么这个系统有什么好处呢?似乎面向对象的方式更加可扩展。
答案与代码易于扩展的方式有关,这是类和代数数据类型之间的紧张关系,Phil Wadler称之为“表达式问题”:
使用代数数据类型,
添加一个新的东西操作非常便宜:只需定义一个新函数。所有旧的对那些东西的函数仍然可以正常工作。
添加一种新型的东西非常昂贵:您必须向现有数据类型添加新的构造函数,并且必须编辑并重新编译使用该类型的每个函数。
使用类,
添加一种新型的东西非常便宜:只需添加一个新子类,并根据需要在该类中为所有现有操作定义专用方法。超类和所有其他子类继续正常工作。
添加一个新的东西操作非常昂贵:您必须向超类添加新的方法声明,并可能向每个现有的子类添加方法定义。在实践中,负担因方法而异。
因此,代数数据类型是封闭的,因为封闭类型可以很好地支持某些程序演变。例如,如果您的数据类型定义了一种语言,则可以轻松添加新的编译器通道,而不会使旧通道失效或更改数据。
虽然有可能具备“开放式”的数据类型,但除非在精心控制的情况下,否则类型检查会变得困难。 Todd Millstein对一种语言设计进行了一些非常优美的工作,该设计支持开放代数类型和可扩展函数,所有这些都带有模块化类型检查器。我发现他的论文非常有趣。
ADT是封闭的事实使得编写总函数变得更加容易。总函数指的是对于其类型的所有可能值,始终会产生结果的函数,例如:
maybeToList :: Maybe a -> [a]
maybeToList Nothing = []
maybeToList (Just x) = [x]
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
类的一个实例来添加自己的类型。
如果你编写了一个函数,例如:
maybeToList Nothing = []
maybeToList (Just x) = [x]
如果你了解,只要涵盖了所有情况就永远不会产生运行时错误。但是,一旦Maybe类型是可扩展的,这个说法就不再成立。在需要可扩展类型的情况下(它们比你想象的要少见),Haskell的规范解决方案是使用类型类。
这个问题(虽然有点老)上有一些出色的答案,但我觉得我必须发表我的意见。
没有办法在不修改声明的情况下以后增加另一个选项。那么这种系统的好处是什么?看起来OO的方式会更加灵活。
我认为,对于开放联合提供的可扩展性,并不总是一个优点,相应地,OO强制你这样做反而是一个弱点。
封闭联合的优势在于其详尽程度:如果您在编译时已经固定了所有备选方案,则可以确定将没有未预料到的情况导致您的代码无法处理。在许多问题领域中,这是一个宝贵的属性,例如,在语言的抽象语法树中。如果您正在编写编译器,语言的表达式将落入一个预定义的、封闭的子用例集中——您不希望用户能够在运行时添加新的子用例,而您的编译器无法理解!
实际上,编译器AST是访问者模式的经典Gang of Four激励示例,这是封闭和详尽模式匹配的OOP对应。反思一下OO程序员最终发明了一个模式来恢复封闭和详尽。
同样,过程式和函数式程序员发明了模式以获得总和效果。最简单的是“函数记录”编码,对应于OO接口。函数记录实际上是一个分派表。 (请注意,C程序员使用这种技术已经有很长时间了!)技巧在于通常有许多可能的给定类型的函数——通常是无限多。因此,如果您有一个记录类型,其字段是函数,则可以轻松支持天文数字或无限个备选方案。而且,由于记录是在运行时创建的,并且可以根据运行时条件灵活地完成,因此备选方案是晚绑定的。
我最后要说的是,在我看来,OO使太多人相信可扩展性与晚绑定是同义词(例如,能够在运行时添加新的子用例到类型中),但这并不一般正确。晚绑定是扩展性的一种技术。另一种技术是组合——使用固定的构建块词汇表和组装它们的规则构建复杂对象。该词汇表和规则理想上非常小,但设计得具有丰富的交互作用,允许您构建非常复杂的事物。
函数式编程——尤其是ML/Haskell静态类型的口味——一直强调组合而非晚绑定。但实际上,两种技术在两种范式中都存在,并且应该是好程序员工具包中的工具。
值得注意的是,编程语言本身就是组合的基本示例。一个编程语言具有有限的、希望是简单的语法,允许您组合它的元素来编写任何可能的程序。(实际上这回到了上面的编译器/访问者模式的例子并激励它。)data Color r b g = Red r | Blue b | Green g
类型,这很容易实现,并且你可以轻松地将其作为单子或函子或其他函数所需的方式来使用。Something a b c = Nada | JustA a | CoupleOf b c
会更好一些。 - rampion好的,这里的“开放”指的是“可以派生自”,而不是像Ruby和Smalltalk那样可以在运行时用新方法扩展类的意义,对吧?
无论如何,请注意两点:首先,在大多数主要基于继承的面向对象语言中,都有一种声明类以限制其继承能力的方法。Java有“final”,在C++中也有一些技巧。因此,在其他面向对象语言中将其作为默认选项只是一个选项。
其次,您仍然可以创建一个使用封闭ADT并添加其他方法或不同实现的新类型。所以你并没有受到那种方式的限制。再次强调,它们似乎在形式上具有相同的强度;在其中一个中可以表达的内容也可以在另一个中表达。
真正的问题在于函数式编程确实是一种不同的范式(“模式”)。如果您期望它应该像面向对象的语言一样,那么您会经常感到惊讶。