为什么ADT好,继承不好?

时间:2022-11-18 22:01:56

I am a long time OO programmer and a functional programming newbie. From my little exposure algebraic data types only look like a special case of inheritance to me where you only have one level hierarchy and the super class cannot be extended outside the module.

我是一个很长时间的OO程序员和功能编程新手。从我的小曝光代数数据类型看起来只是一个特殊的继承情况,我只有一个级别的层次结构,超级类不能扩展到模块之外。

So my (potentially dumb) question is: If ADTs are just that, a special case of inheritance (again this assumption may be wrong; please correct me in that case), then why does inheritance gets all the criticism and ADTs get all the praise?

所以我的(可能是愚蠢的)问题是:如果ADT只是那个,一个特殊的继承案例(再次这个假设可能是错误的;请在那种情况下纠正我),那么为什么继承得到所有的批评和ADT得到所有的赞美?

Thank you.

谢谢。

4 个解决方案

#1


25  

I think that ADTs are complementary to inheritance. Both of them allow you to create extensible code, but the way the extensibility works is different:

我认为ADT是对继承的补充。它们都允许您创建可扩展代码,但可扩展性的工作方式不同:

  • ADTs make it easy to add new functionality for working with existing types
    • You can easily add new function that works with ADT, which has a fixed set of cases
    • 您可以轻松添加适用于ADT的新功能,ADT具有一组固定的案例
    • On the other hand, adding new case requires modifying all functions
    • 另一方面,添加新案例需要修改所有功能
  • ADT可以轻松添加用于处理现有类型的新功能您可以轻松添加适用于ADT的新功能,ADT具有固定的案例集另一方面,添加新案例需要修改所有功能
  • Inheritance makes it easy to add new types when you have fixed functionality
    • You can easily create inherited class and implement fixed set of virtual functions
    • 您可以轻松创建继承的类并实现固定的虚拟函数集
    • On the other hand, adding a new virtual function requires modifying all inherited classes
    • 另一方面,添加新的虚函数需要修改所有继承的类
  • 当您具有固定功能时,继承可以轻松添加新类型您可以轻松创建继承类并实现固定的虚拟函数集另一方面,添加新的虚函数需要修改所有继承的类

Both object-oriented world and functional world developed their ways to allow the other type of extensibility. In Haskell, you can use typeclasses, in ML/OCaml, people would use dictionary of functions or maybe (?) functors to get the inhertiance-style extensibility. On the other hand, in OOP, people use the Visitor pattern, which is essentially a way to get something like ADTs.

面向对象的世界和功能世界都开发了允许其他类型的可扩展性的方法。在Haskell中,你可以使用类型类,在ML / OCaml中,人们会使用函数字典或者(?)仿函数来获得inhertiance样式的可扩展性。另一方面,在OOP中,人们使用访问者模式,这实际上是一种获取类似ADT的方法。

The usual programming patterns are different in OOP and FP, so when you're programming in a functional language, you're writing the code in a way that requires the functional-style extensibility more often (and similarly in OOP). In practice, I think it is great to have a language that allows you to use both of the styles depending on the problem you're trying to solve.

通常的编程模式在OOP和FP中是不同的,因此当您使用函数式语言进行编程时,您将以更频繁地需要函数式可扩展性的方式编写代码(并且在OOP中类似)。在实践中,我认为有一种语言可以让你根据你想要解决的问题使用这两种风格,这很棒。

#2


9  

Tomas Petricek has got the fundamentals exactly right; you might also want to look at Phil Wadler's writing on the "expression problem".

Tomas Petricek完全正确地掌握了基本面;你可能也想看看Phil Wadler关于“表达问题”的写作。

There are two other reasons some of us prefer algebraic data types over inheritance:

还有另外两个原因我们中的一些人更喜欢代数数据类型而不是继承:

  • Using algebraic data types, the compiler can (and does) tell you if you have forgotten a case or if a case is redundant. This ability is especially useful when there are many more operations on things than there are kinds of thing. (E.g., many more functions than algebraic datatypes, or many more methods than OO constructors.) In an object-oriented language, if you leave a method out of a subclass, the compiler can't tell whether that's a mistake or whether you intended to inherit the superclass method unchanged.

    使用代数数据类型,编译器可以(并且确实)告诉您是否忘记了案例或案例是多余的。当对事物进行更多操作而不是有各种各样的事情时,这种能力特别有用。 (例如,比代数数据类型更多的函数,或者比OO构造函数更多的方法。)在面向对象的语言中,如果将方法保留在子类之外,编译器无法判断这是错误还是您打算继承超类方法不变。

  • This one is more subjective: many people have noted that if inheritance is used properly and aggressively, the implementation of an algorithm can easily be smeared out over a half a dozen classes, and even with a nice class browser at can be hard to follow the logic of the program (data flow and control flow). Without a nice class browser, you have no chance. If you want to see a good example, try implementing bignums in Smalltalk, with automatic failover to bignums on overflow. It's a great abstraction, but the language makes the implementation difficult to follow. Using functions on algebraic data types, the logic of your algorithm is usually all in one place, or if it is split up, its split up into functions which have contracts that are easy to understand.

    这个更主观:很多人都注意到如果继承被正确使用并且积极地使用,算法的实现很容易被涂抹在六个以上的类中,即使有一个很好的类浏览器也很难遵循程序的逻辑(数据流和控制流)。没有一个很好的类浏览器,你没有机会。如果你想看到一个很好的例子,尝试在Smalltalk中实现bignums,并在溢出时自动故障转移到bignums。这是一个很好的抽象,但语言使得实现很难遵循。使用代数数据类型的函数,算法的逻辑通常都在一个地方,或者如果它被拆分,它就会分成具有易于理解的契约的函数。


P.S. What are you reading? I don't know of any responsible person who says "ADTs good; OO bad."

附:你在读什么?我不知道有任何负责人说“ADT好,不好”。

#3


8  

In my experience, what people usually consider "bad" about inheritance as implemented by most OO languages is not the idea of inheritance itself but the idea of subclasses modifying the behavior of methods defined in the superclass (method overriding), specifically in the presence of mutable state. It's really the last part that's the kicker. Most OO languages treat objects as "encapsulating state," which amounts to allowing rampant mutation of state inside of objects. So problems arise when, for example, a superclass expects a certain method to modify a private variable, but a subclass overrides the method to do something completely different. This can introduce subtle bugs which the compiler is powerless to prevent.

根据我的经验,人们通常认为大多数OO语言实现的继承“坏”并不是继承本身的想法,而是子类修改超类中定义的方法行为(方法重写)的想法,特别是在存在的情况下可变状态。这真是最后一部分是踢球者。大多数OO语言将对象视为“封装状态”,这相当于允许对象内部状态的猖獗变异。因此,例如,当超类期望某个方法修改私有变量时会出现问题,但是子类会覆盖该方法以完成不同的操作。这可能会引入编译器无法阻止的细微错误。

Note that in Haskell's implementation of subclass polymorphism, mutable state is disallowed, so you don't have such issues.

请注意,在Haskell的子类多态的实现中,不允许使用可变状态,因此您不会遇到此类问题。

Also, see this objection to the concept of subtyping.

另外,请参阅对subtyping概念的这种反对意见。

#4


7  

I am a long time OO programmer and a functional programming newbie. From my little exposure algebraic data types only look like a special case of inheritance to me where you only have one level hierarchy and the super class cannot be extended outside the module.

我是一个很长时间的OO程序员和功能编程新手。从我的小曝光代数数据类型看起来只是一个特殊的继承情况,我只有一个级别的层次结构,超级类不能扩展到模块之外。

You are describing closed sum types, the most common form of algebraic data types, as seen in F# and Haskell. Basically, everyone agrees that they are a useful feature to have in the type system, primarily because pattern matching makes it easy to dissect them by shape as well as by content and also because they permit exhaustiveness and redundancy checking.

您正在描述闭合和类型,这是代数数据类型的最常见形式,如F#和Haskell中所示。基本上,每个人都同意它们在类型系统中是一个有用的特性,主要是因为模式匹配使得通过形状和内容来分析它们变得容易,并且因为它们允许详尽和冗余检查。

However, there are other forms of algebraic datatypes. An important limitation of the conventional form is that they are closed, meaning that a previously-defined closed sum type cannot be extended with new type constructors (part of a more general problem known as "the expression problem"). OCaml's polymorphic variants allow both open and closed sum types and, in particular, the inference of sum types. In contrast, Haskell and F# cannot infer sum types. Polymorphic variants solve the expression problem and they are extremely useful. In fact, some languages are built entirely on extensible algebraic data types rather than closed sum types.

但是,还有其他形式的代数数据类型。传统形式的一个重要限制是它们是封闭的,这意味着先前定义的闭合和类型不能用新类型构造函数扩展(更常见问题的一部分称为“表达式问题”)。 OCaml的多态变体允许开放和闭合和类型,特别是和类型的推断。相比之下,Haskell和F#无法推断和类型。多态变体解决了表达问题,它们非常有用。事实上,有些语言完全是基于可扩展的代数数据类型而不是封闭的和类型。

In the extreme, you also have languages like Mathematica where "everything is an expression". Thus the only type in the type system forms a trivial "singleton" algebra. This is "extensible" in the sense that it is infinite and, again, it culminates in a completely different style of programming.

在极端情况下,您还拥有像Mathematica这样的语言,其中“一切都是表达”。因此,类型系统中唯一的类型形成了一个简单的“单例”代数。这是“可扩展的”,因为它是无限的,并且它最终以完全不同的编程风格达到顶峰。

So my (potentially dumb) question is: If ADTs are just that, a special case of inheritance (again this assumption may be wrong; please correct me in that case), then why does inheritance gets all the criticism and ADTs get all the praise?

所以我的(可能是愚蠢的)问题是:如果ADT只是那个,一个特殊的继承案例(再次这个假设可能是错误的;请在那种情况下纠正我),那么为什么继承得到所有的批评和ADT得到所有的赞美?

I believe you are referring specifically to implementation inheritance (i.e. overriding functionality from a parent class) as opposed to interface inheritance (i.e. implementing a consistent interface). This is an important distinction. Implementation inheritance is often hated whereas interface inheritance is often loved (e.g. in F# which has a limited form of ADTs).

我相信你特指的是实现继承(即来自父类的覆盖功能),而不是接口继承(即实现一致的接口)。这是一个重要的区别。实现继承经常被讨厌,而界面继承经常被喜欢(例如,在具有有限形式的ADT的F#中)。

You really want both ADTs and interface inheritance. Languages like OCaml and F# offer both.

你真的想要ADT和接口继承。 OCaml和F#等语言都提供。

#1


25  

I think that ADTs are complementary to inheritance. Both of them allow you to create extensible code, but the way the extensibility works is different:

我认为ADT是对继承的补充。它们都允许您创建可扩展代码,但可扩展性的工作方式不同:

  • ADTs make it easy to add new functionality for working with existing types
    • You can easily add new function that works with ADT, which has a fixed set of cases
    • 您可以轻松添加适用于ADT的新功能,ADT具有一组固定的案例
    • On the other hand, adding new case requires modifying all functions
    • 另一方面,添加新案例需要修改所有功能
  • ADT可以轻松添加用于处理现有类型的新功能您可以轻松添加适用于ADT的新功能,ADT具有固定的案例集另一方面,添加新案例需要修改所有功能
  • Inheritance makes it easy to add new types when you have fixed functionality
    • You can easily create inherited class and implement fixed set of virtual functions
    • 您可以轻松创建继承的类并实现固定的虚拟函数集
    • On the other hand, adding a new virtual function requires modifying all inherited classes
    • 另一方面,添加新的虚函数需要修改所有继承的类
  • 当您具有固定功能时,继承可以轻松添加新类型您可以轻松创建继承类并实现固定的虚拟函数集另一方面,添加新的虚函数需要修改所有继承的类

Both object-oriented world and functional world developed their ways to allow the other type of extensibility. In Haskell, you can use typeclasses, in ML/OCaml, people would use dictionary of functions or maybe (?) functors to get the inhertiance-style extensibility. On the other hand, in OOP, people use the Visitor pattern, which is essentially a way to get something like ADTs.

面向对象的世界和功能世界都开发了允许其他类型的可扩展性的方法。在Haskell中,你可以使用类型类,在ML / OCaml中,人们会使用函数字典或者(?)仿函数来获得inhertiance样式的可扩展性。另一方面,在OOP中,人们使用访问者模式,这实际上是一种获取类似ADT的方法。

The usual programming patterns are different in OOP and FP, so when you're programming in a functional language, you're writing the code in a way that requires the functional-style extensibility more often (and similarly in OOP). In practice, I think it is great to have a language that allows you to use both of the styles depending on the problem you're trying to solve.

通常的编程模式在OOP和FP中是不同的,因此当您使用函数式语言进行编程时,您将以更频繁地需要函数式可扩展性的方式编写代码(并且在OOP中类似)。在实践中,我认为有一种语言可以让你根据你想要解决的问题使用这两种风格,这很棒。

#2


9  

Tomas Petricek has got the fundamentals exactly right; you might also want to look at Phil Wadler's writing on the "expression problem".

Tomas Petricek完全正确地掌握了基本面;你可能也想看看Phil Wadler关于“表达问题”的写作。

There are two other reasons some of us prefer algebraic data types over inheritance:

还有另外两个原因我们中的一些人更喜欢代数数据类型而不是继承:

  • Using algebraic data types, the compiler can (and does) tell you if you have forgotten a case or if a case is redundant. This ability is especially useful when there are many more operations on things than there are kinds of thing. (E.g., many more functions than algebraic datatypes, or many more methods than OO constructors.) In an object-oriented language, if you leave a method out of a subclass, the compiler can't tell whether that's a mistake or whether you intended to inherit the superclass method unchanged.

    使用代数数据类型,编译器可以(并且确实)告诉您是否忘记了案例或案例是多余的。当对事物进行更多操作而不是有各种各样的事情时,这种能力特别有用。 (例如,比代数数据类型更多的函数,或者比OO构造函数更多的方法。)在面向对象的语言中,如果将方法保留在子类之外,编译器无法判断这是错误还是您打算继承超类方法不变。

  • This one is more subjective: many people have noted that if inheritance is used properly and aggressively, the implementation of an algorithm can easily be smeared out over a half a dozen classes, and even with a nice class browser at can be hard to follow the logic of the program (data flow and control flow). Without a nice class browser, you have no chance. If you want to see a good example, try implementing bignums in Smalltalk, with automatic failover to bignums on overflow. It's a great abstraction, but the language makes the implementation difficult to follow. Using functions on algebraic data types, the logic of your algorithm is usually all in one place, or if it is split up, its split up into functions which have contracts that are easy to understand.

    这个更主观:很多人都注意到如果继承被正确使用并且积极地使用,算法的实现很容易被涂抹在六个以上的类中,即使有一个很好的类浏览器也很难遵循程序的逻辑(数据流和控制流)。没有一个很好的类浏览器,你没有机会。如果你想看到一个很好的例子,尝试在Smalltalk中实现bignums,并在溢出时自动故障转移到bignums。这是一个很好的抽象,但语言使得实现很难遵循。使用代数数据类型的函数,算法的逻辑通常都在一个地方,或者如果它被拆分,它就会分成具有易于理解的契约的函数。


P.S. What are you reading? I don't know of any responsible person who says "ADTs good; OO bad."

附:你在读什么?我不知道有任何负责人说“ADT好,不好”。

#3


8  

In my experience, what people usually consider "bad" about inheritance as implemented by most OO languages is not the idea of inheritance itself but the idea of subclasses modifying the behavior of methods defined in the superclass (method overriding), specifically in the presence of mutable state. It's really the last part that's the kicker. Most OO languages treat objects as "encapsulating state," which amounts to allowing rampant mutation of state inside of objects. So problems arise when, for example, a superclass expects a certain method to modify a private variable, but a subclass overrides the method to do something completely different. This can introduce subtle bugs which the compiler is powerless to prevent.

根据我的经验,人们通常认为大多数OO语言实现的继承“坏”并不是继承本身的想法,而是子类修改超类中定义的方法行为(方法重写)的想法,特别是在存在的情况下可变状态。这真是最后一部分是踢球者。大多数OO语言将对象视为“封装状态”,这相当于允许对象内部状态的猖獗变异。因此,例如,当超类期望某个方法修改私有变量时会出现问题,但是子类会覆盖该方法以完成不同的操作。这可能会引入编译器无法阻止的细微错误。

Note that in Haskell's implementation of subclass polymorphism, mutable state is disallowed, so you don't have such issues.

请注意,在Haskell的子类多态的实现中,不允许使用可变状态,因此您不会遇到此类问题。

Also, see this objection to the concept of subtyping.

另外,请参阅对subtyping概念的这种反对意见。

#4


7  

I am a long time OO programmer and a functional programming newbie. From my little exposure algebraic data types only look like a special case of inheritance to me where you only have one level hierarchy and the super class cannot be extended outside the module.

我是一个很长时间的OO程序员和功能编程新手。从我的小曝光代数数据类型看起来只是一个特殊的继承情况,我只有一个级别的层次结构,超级类不能扩展到模块之外。

You are describing closed sum types, the most common form of algebraic data types, as seen in F# and Haskell. Basically, everyone agrees that they are a useful feature to have in the type system, primarily because pattern matching makes it easy to dissect them by shape as well as by content and also because they permit exhaustiveness and redundancy checking.

您正在描述闭合和类型,这是代数数据类型的最常见形式,如F#和Haskell中所示。基本上,每个人都同意它们在类型系统中是一个有用的特性,主要是因为模式匹配使得通过形状和内容来分析它们变得容易,并且因为它们允许详尽和冗余检查。

However, there are other forms of algebraic datatypes. An important limitation of the conventional form is that they are closed, meaning that a previously-defined closed sum type cannot be extended with new type constructors (part of a more general problem known as "the expression problem"). OCaml's polymorphic variants allow both open and closed sum types and, in particular, the inference of sum types. In contrast, Haskell and F# cannot infer sum types. Polymorphic variants solve the expression problem and they are extremely useful. In fact, some languages are built entirely on extensible algebraic data types rather than closed sum types.

但是,还有其他形式的代数数据类型。传统形式的一个重要限制是它们是封闭的,这意味着先前定义的闭合和类型不能用新类型构造函数扩展(更常见问题的一部分称为“表达式问题”)。 OCaml的多态变体允许开放和闭合和类型,特别是和类型的推断。相比之下,Haskell和F#无法推断和类型。多态变体解决了表达问题,它们非常有用。事实上,有些语言完全是基于可扩展的代数数据类型而不是封闭的和类型。

In the extreme, you also have languages like Mathematica where "everything is an expression". Thus the only type in the type system forms a trivial "singleton" algebra. This is "extensible" in the sense that it is infinite and, again, it culminates in a completely different style of programming.

在极端情况下,您还拥有像Mathematica这样的语言,其中“一切都是表达”。因此,类型系统中唯一的类型形成了一个简单的“单例”代数。这是“可扩展的”,因为它是无限的,并且它最终以完全不同的编程风格达到顶峰。

So my (potentially dumb) question is: If ADTs are just that, a special case of inheritance (again this assumption may be wrong; please correct me in that case), then why does inheritance gets all the criticism and ADTs get all the praise?

所以我的(可能是愚蠢的)问题是:如果ADT只是那个,一个特殊的继承案例(再次这个假设可能是错误的;请在那种情况下纠正我),那么为什么继承得到所有的批评和ADT得到所有的赞美?

I believe you are referring specifically to implementation inheritance (i.e. overriding functionality from a parent class) as opposed to interface inheritance (i.e. implementing a consistent interface). This is an important distinction. Implementation inheritance is often hated whereas interface inheritance is often loved (e.g. in F# which has a limited form of ADTs).

我相信你特指的是实现继承(即来自父类的覆盖功能),而不是接口继承(即实现一致的接口)。这是一个重要的区别。实现继承经常被讨厌,而界面继承经常被喜欢(例如,在具有有限形式的ADT的F#中)。

You really want both ADTs and interface inheritance. Languages like OCaml and F# offer both.

你真的想要ADT和接口继承。 OCaml和F#等语言都提供。