为什么要避免子类型化?

36

我在Scala社区中看到很多人建议避免子类型化“像瘟疫一样”。使用子类型化的各种原因是什么?有哪些替代方案?

8个回答

19

类型决定组合的粒度,即可扩展性的粒度。

例如,一个接口,比如Comparable,结合了(因此混淆了)相等和关系运算符。因此,无法只在相等或关系接口上进行组合。

通常,继承的替换原则是不可判定的。罗素悖论意味着任何可扩展的集合(即不枚举每个可能成员或子类型的类型),都可以包含自身,即是其自身的子类型。但为了确定什么是子类型而不是自身,必须完全枚举其自身的不变量,因此它不再具有可扩展性。这就是子类型扩展性使继承变得不可判定的悖论。这种悖论必须存在,否则知识将是静态的,因此知识形成将不存在

函数组合是子类型的满射替换,因为函数的输入可以替换为其输出,即任何期望输出类型的地方,都可以通过将其包装在函数调用中来替换输入类型。但组合并不会产生子类型的双射契约——访问函数输出的接口不会访问函数输入实例。

因此,组合不必保持未来(即无界)的不变量,因此可以既可扩展又可决策。子类型可以更有力,如果它是可证明决策的,因为它保持这个双射契约,例如,对于超类型的不可变列表进行排序的函数可以在子类型的不可变列表上操作。

因此,结论是列举每个类型(即其接口)的所有不变量,使这些类型正交(最大限度地增加组合的粒度),然后使用函数组合来完成那些不可正交的不变量的扩展。因此,只有在确定子类型模型符合超类型接口的不变量时,子类型才是适当的,并且子类型的附加接口相对于超类型接口的不变量是正交的。因此,接口的不变量应该是正交的。

范畴论提供了规则,用于每个子类型的不变量的模型,即Functor、Applicative和Monad的模型,这些模型在提升类型上保持函数组合,例如,参见上述子类型列表示例的强大功能。


1
关于我的观点,子类型化可能更加强大的阐述,请点击此链接:Elaboration - Shelby Moore III
1
独立可扩展的子类型需要一个完整的解决方案来解决表达式问题。我在一个答案中指出,异构类型列表元素可以通过联合实现(它们不需要有相互的超类型),而不是答案中提到的虚拟继承的“强大”功能。 - Shelby Moore III
1
“表达式引理”,Ralf Lammel和Ondrej Rypacek。 - Shelby Moore III
2
最近的想法:(http://groups.google.com/group/scala-language/browse_thread/thread/3c886611e320f8cb)。 - Shelby Moore III
1
我已经清楚地看到了“大局”。 - Shelby Moore III
最近在scala-lang的Google组中,我解释了为什么子类化是一种反模式(谷歌“scala-lang shelby”)。请注意,子类化不是子类型化,因为前者是一个不变量,在操作语义层上得到强制执行。 - Shelby Moore III

9

一个原因是在使用子类型时,equals() 很难做到完全正确。请参考Java中如何编写等式方法文章的“陷阱#4:未将 equals 定义为等价关系”。简而言之:要在使用子类型时正确处理相等性,需要进行双重调度。


1
@Oliver:是的,它确实做到了。但这正是我所说的双重分派。 - michid

5
我认为一般情况下要使语言尽可能“纯净”(即尽可能使用纯函数),这与Haskell的比较有关。
来自“一个程序员的深思熟虑”。

Scala是一种混合了面向对象和函数式编程的语言,必须处理像子类型这样的问题(Haskell没有这个问题)。

PSE答案中所述:

没有办法限制子类型,以便它不能做比继承自基类的类型更多的事情。
例如,如果基类是不可变的,并定义了一个纯方法foo(...),派生类不能是可变的,也不能用不是纯的函数覆盖foo()

但实际上建议根据当前开发的程序使用最适合的解决方案。

4
专注于子类型,忽略与类、继承、OOP等相关的问题。我们认为子类型表示类型之间的isa关系。例如,类型A和B具有不同的操作,但如果A isa B,则我们可以在A上使用任何B的操作。
另一方面,使用另一种传统关系,如果C hasa B,则我们可以在C上重用B的任何操作。通常语言会让你以更好的语法来编写一个.opOnB,而不是像组合的情况下那样写a.super.opOnB或c.b.opOnB。
问题在于,在许多情况下,有多种方法来关联两种类型。例如,假设0是虚部,则可以将Real嵌入Complex中,但是通过忽略虚部,也可以将Complex嵌入Real中,因此两者都可以看作是对方的子类型,并且子类型强制执行将一种关系视为首选。此外,还有更多可能的关系(例如,使用极坐标表示法的theta分量将Complex视为Real)。
在正式术语中,我们通常称这些类型之间的关系为morphisms,并且对于具有不同属性的关系有特殊的morphisms(例如同构,同态)。
在具有子类型的语言中,通常会在isa关系上添加更多的语法糖,并且鉴于许多可能的嵌入,每当我们使用不首选的关系时,我们往往会看到不必要的摩擦。如果将继承、类和OOP引入其中,则问题变得更加明显和混乱。

+1,你的观点与Runar在这篇文章中提出的观点相吻合。然而我仍然不太确定。我需要例子。 - missingfaktor

2
我不懂Scala,但我认为“优先选择组合而非继承”的口号对于Scala与其他面向对象编程语言一样适用(子类型经常与“继承”具有相同的含义)。在这里,您可以找到更多信息:Prefer composition over inheritance?

2
我的回答并没有回答为什么要避免,而是试图给出另一个提示,说明为什么可以避免。
使用“类型类”,您可以在不修改现有类型/类的情况下添加对它们的抽象。继承用于表示某些类是更抽象类的特殊化。但是,使用类型类,您可以获取任何现有类,并表达它们共享的公共属性,例如它们是可比较的。只要您不关心它们是否可比较,您甚至都不会注意到它。只要您不使用这些方法,这些类就不会从某个抽象的可比较类型继承任何方法。这有点像在动态语言中编程。
进一步阅读:

http://blog.tmorris.net/the-power-of-type-classes-with-scala-implicit-defs/

http://debasishg.blogspot.com/2010/07/refactoring-into-scala-type-classes.html


1

我认为很多Scala程序员都是以前的Java程序员。他们习惯于从面向对象的子类型思考问题,因此应该能够轻松地找到大多数问题的面向对象解决方案。但是函数式编程是一种新的范式,需要探索,因此人们要求不同类型的解决方案。


1

这篇是我在这个主题上找到的最好的论文。来自论文的激励性语录 -

我们认为,虽然一些更简单的面向对象语言方面与ML兼容,但将一个完整的基于类的对象系统添加到ML中会导致过度复杂的类型系统和相对较少的表达能力增益。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接