Зачем избегать подтипов?

Я видел много людей в сообществе Scala, которые советуют избегать подтипов "как чума". Каковы различные причины против использования подтипов? Какие есть альтернативы?

8 ответов

Решение

Типы определяют гранулярность композиции, то есть расширяемость.

Например, интерфейс, например Comparable, который объединяет (таким образом, объединяет) операторы равенства и отношения. Таким образом, невозможно составить только один интерфейс равенства или реляционный интерфейс.

В общем, принцип замещения наследования неразрешим. Парадокс Рассела подразумевает, что любой набор, который является расширяемым (то есть не перечисляет тип каждого возможного члена или подтипа), может включать себя, то есть является подтипом самого себя. Но для того, чтобы идентифицировать (решить), что является подтипом, а не самим собой, инварианты самого себя должны быть полностью перечислены, таким образом, он больше не является расширяемым. Это парадокс, что подтипная расширяемость делает наследование неразрешимым. Этот парадокс должен существовать, иначе знание будет статичным и, следовательно, формирование знаний не будет существовать.

Композиция функции - это сюръективная подстановка подтипа, потому что вход функции может быть заменен ее выводом, т. Е. В любом месте, где ожидается тип вывода, тип ввода можно заменить, заключив его в вызов функции. Но композиция не делает биективный контракт подтипирования - доступ к интерфейсу вывода функции, не получает доступ к входному экземпляру функции.

Таким образом, композиция не должна поддерживать будущие (то есть неограниченные) инварианты и, следовательно, может быть как расширяемой, так и разрешимой. Субтипирование может быть НАМНОГО более мощным, если оно доказуемо разрешимо, потому что оно поддерживает этот биективный контракт, например, функция, которая сортирует неизменный список супертипа, может работать с неизменным списком подтипа.

Таким образом, вывод состоит в том, чтобы перечислить все инварианты каждого типа (то есть его интерфейсов), сделать эти типы ортогональными (максимизировать гранулярность композиции), а затем использовать композицию функций для выполнения расширения, где эти инварианты не будут ортогональными. Таким образом, подтип подходит только в том случае, если он доказуемо моделирует инварианты интерфейса супертипа, а дополнительный интерфейс (ы) подтипа доказуемо ортогональны инвариантам интерфейса супертипа. Таким образом, инварианты интерфейсов должны быть ортогональными.

Теория категорий предоставляет правила для модели инвариантов каждого подтипа, то есть Functor, Applicative и Monad, которые сохраняют композицию функций на поднятых типах, т.е. см. Вышеупомянутый пример мощности подтипов для списков.

Одна из причин заключается в том, что equals() очень трудно понять правильно, когда используется подтипирование. См. Как написать метод равенства в Java. В частности, "Подводный камень #4: Неспособность определить равные как отношение эквивалентности". По сути: чтобы получить равенство прямо под подтипом, вам нужна двойная отправка.

Я думаю, что общий контекст для того, чтобы язык был настолько "чистым", насколько это возможно (т. Е. Используя как можно больше чистых функций), и основан на сравнении с Haskell.
Из " Размышления программиста"

Scala, будучи гибридным языком OO-FP, должен позаботиться о таких проблемах, как подтипы (которых у Haskell нет).

Как упомянуто в этом ответе PSE:

нет способа ограничить подтип, чтобы он не мог делать больше, чем тип, от которого он наследуется.
Например, если базовый класс неизменен и определяет чистый метод foo(...)производные классы не должны быть изменяемыми или переопределять foo() с функцией, которая не является чистой

Но реальная рекомендация будет заключаться в том, чтобы использовать лучшее решение, адаптированное к программе, которую вы сейчас разрабатываете.

Сосредоточение внимания на подтипах, игнорирование вопросов, связанных с классами, наследованием, ООП и т. Д. У нас есть идея, что подтип представляет собой отношение isa между типами. Например, типы A и B имеют разные операции, но если A является B, мы можем использовать любую из операций B на A.

OTOH, используя другое традиционное отношение, если C имеет B, то мы можем повторно использовать любую из операций B на C. Обычно языки позволяют вам писать с более хорошим синтаксисом, a.opOnB вместо a.super.opOnB, как это было бы в кейс композиции, cbopOnB

Проблема в том, что во многих случаях существует более одного способа связать два типа. Например, Real может быть встроен в Complex, предполагая 0 в мнимой части, но Complex может быть встроен в Real, игнорируя мнимую часть, поэтому оба могут рассматриваться как подтипы других, а силы подтипов одно отношение должны рассматриваться как предпочтительные. Кроме того, есть более возможные отношения (например, рассматривать Complex как вещественное с использованием тэта-компонента полярного представления).

В формальной терминологии мы обычно говорим морфизм к таким отношениям между типами, и существуют специальные виды морфизмов для отношений с различными свойствами (например, изоморфизм, гомоморфизм).

В языке с подтипами обычно гораздо больше сахара в отношениях isa, и, учитывая множество возможных вложений, мы склонны видеть ненужные трения всякий раз, когда используем непривязанное отношение. Если мы добавим наследование, классы и ООП, проблема станет намного более заметной и запутанной.

Я не знаю Scala, но я думаю, что мантра "предпочитаешь композицию, а не наследование" применяется для Scala точно так же, как и для любого другого языка программирования ОО (а подтипы часто используются в том же значении, что и "наследование"). Вот

Предпочитаете композицию наследству?

Вы найдете больше информации.

Мой ответ не отвечает, почему его избегают, но пытается дать еще один намек на то, почему его можно избежать.

Используя "классы типов", вы можете добавить абстракцию поверх существующих типов / классов без их изменения. Наследование используется для выражения того, что некоторые классы являются специализациями более абстрактного класса. Но с классами типов вы можете взять любые существующие классы и выразить, что все они имеют общее свойство, например, они Comparable, И до тех пор, пока вы не обеспокоены тем, Comparable Вы даже не замечаете этого. Классы не наследуют никаких методов от некоторых абстрактных Comparable печатайте до тех пор, пока вы их не используете. Это немного похоже на программирование на динамических языках.

Далее читает:

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 приводит к чрезмерно сложной системе типов и относительно небольшому выразительному выигрышу.

Другие вопросы по тегам