我在Scala社区中看到很多人建议避免子类型化“像瘟疫一样”。使用子类型化的各种原因是什么?有哪些替代方案?
我在Scala社区中看到很多人建议避免子类型化“像瘟疫一样”。使用子类型化的各种原因是什么?有哪些替代方案?
类型决定组合的粒度,即可扩展性的粒度。
例如,一个接口,比如Comparable,结合了(因此混淆了)相等和关系运算符。因此,无法只在相等或关系接口上进行组合。
通常,继承的替换原则是不可判定的。罗素悖论意味着任何可扩展的集合(即不枚举每个可能成员或子类型的类型),都可以包含自身,即是其自身的子类型。但为了确定什么是子类型而不是自身,必须完全枚举其自身的不变量,因此它不再具有可扩展性。这就是子类型扩展性使继承变得不可判定的悖论。这种悖论必须存在,否则知识将是静态的,因此知识形成将不存在。
函数组合是子类型的满射替换,因为函数的输入可以替换为其输出,即任何期望输出类型的地方,都可以通过将其包装在函数调用中来替换输入类型。但组合并不会产生子类型的双射契约——访问函数输出的接口不会访问函数输入实例。
因此,组合不必保持未来(即无界)的不变量,因此可以既可扩展又可决策。子类型可以更有力,如果它是可证明决策的,因为它保持这个双射契约,例如,对于超类型的不可变列表进行排序的函数可以在子类型的不可变列表上操作。因此,结论是列举每个类型(即其接口)的所有不变量,使这些类型正交(最大限度地增加组合的粒度),然后使用函数组合来完成那些不可正交的不变量的扩展。因此,只有在确定子类型模型符合超类型接口的不变量时,子类型才是适当的,并且子类型的附加接口相对于超类型接口的不变量是正交的。因此,接口的不变量应该是正交的。
范畴论提供了规则,用于每个子类型的不变量的模型,即Functor、Applicative和Monad的模型,这些模型在提升类型上保持函数组合,例如,参见上述子类型列表示例的强大功能。
一个原因是在使用子类型时,equals() 很难做到完全正确。请参考Java中如何编写等式方法文章的“陷阱#4:未将 equals 定义为等价关系”。简而言之:要在使用子类型时正确处理相等性,需要进行双重调度。
如PSE答案中所述:Scala是一种混合了面向对象和函数式编程的语言,必须处理像子类型这样的问题(Haskell没有这个问题)。
但实际上建议根据当前开发的程序使用最适合的解决方案。没有办法限制子类型,以便它不能做比继承自基类的类型更多的事情。
例如,如果基类是不可变的,并定义了一个纯方法foo(...)
,派生类不能是可变的,也不能用不是纯的函数覆盖foo()
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
我认为很多Scala程序员都是以前的Java程序员。他们习惯于从面向对象的子类型思考问题,因此应该能够轻松地找到大多数问题的面向对象解决方案。但是函数式编程是一种新的范式,需要探索,因此人们要求不同类型的解决方案。
这篇是我在这个主题上找到的最好的论文。来自论文的激励性语录 -
我们认为,虽然一些更简单的面向对象语言方面与ML兼容,但将一个完整的基于类的对象系统添加到ML中会导致过度复杂的类型系统和相对较少的表达能力增益。