Я видел, как многие люди в сообществе Scala советуют избегать подтипов "как чума". Каковы различные причины против использования подтипов? Каковы альтернативы?
Зачем избегать подтипов?
Ответ 1
Типы определяют зернистость композиции, то есть расширяемость.
Например, интерфейс, например. Сопоставимый, который объединяет (таким образом, объединяет) равенства и реляционные операторы. Таким образом, невозможно составить только одно из равенства или реляционного интерфейса.
В общем, принцип замены наследования неразрешимый. Парадокс Рассела подразумевает, что любой набор, который является расширяемым (т.е. Не перечисляет тип каждого возможного члена или подтипа), может включать сам себя, т.е. Сам подтип. Но для того, чтобы идентифицировать (решать) то, что является подтипом, а не собой, инварианты самого себя должны быть полностью перечислены, поэтому он уже не расширяется. Это парадокс, заключающийся в том, что подтипированная расширяемость делает наследование неразрешимым. Этот парадокс должен существовать, иначе знание статично и, следовательно, формирование знаний не будет существовать.
Состав функций - это сюръективная подстановка подтипирования, поскольку входной сигнал функции может быть заменен на его выход, т.е. любой, где ожидается тип вывода, тип ввода может быть заменен путем обертывания его в вызове функции. Но композиция не делает биективный контракт подтипирования - доступ к интерфейсу вывода функции, не имеет доступа к входному экземпляру функции.
Таким образом, композиция не должна поддерживать будущих (т.е. неограниченных) инвариантов и, следовательно, может быть как расширяемой, так и разрешимой. Подтипирование может быть MUCH более мощным, если оно можно разрешить, поскольку оно поддерживает этот биективный контракт, например. функция, которая сортирует неизменяемый список супертипа, может работать с неизменяемым списком подтипа.
Таким образом, вывод состоит в том, чтобы перечислить все инварианты каждого типа (т.е. его интерфейсов), сделать эти типы ортогональными (максимизировать гранулярность композиции), а затем использовать функциональный состав для выполнения расширения, где эти инварианты не будут ортогональными. Таким образом, подтип подходит только там, где он, по-видимому, моделирует инварианты интерфейса супертипа, а дополнительный интерфейс подтипа доказуемо ортогонален инвариантам интерфейса супертипа. Таким образом, инварианты интерфейсов должны быть ортогональными.
Теория категорий содержит правила для модели инвариантов каждого подтипа, то есть Functor, Applicative и Monad, которые сохранить композицию функции на поднятых типах, т.е. увидеть вышеупомянутый пример мощности подтипирования для списков.
Ответ 2
Одна из причин заключается в том, что equals() очень сложно получить право при включении подтипа. См. Как написать метод равенства в Java. В частности, "Pitfall №4: невозможность определить равные в качестве отношения эквивалентности". В сущности: чтобы получить право прямо под подтипом, вам нужна двойная отправка.
Ответ 3
Я думаю, что общий контекст заключается в том, чтобы lanaguage был как можно более "чистым" (т.е. максимально используя чистые функции), и происходит от сравнения с Haskell.
От " Соображения программиста"
Scala, являющийся гибридным языком OO-FP, должен заботиться о таких проблемах, как подтипирование (которое Haskell не имеет).
Как упоминалось в этом ответе PSE:
нет способа ограничить подтип, чтобы он не мог делать больше, чем тот, на котором он наследует.
Например, если базовый класс неизменен и определяет чистый методfoo(...)
, производные классы не должны изменяться или переопределятьfoo()
с функцией, которая не является чистой
Но фактическая рекомендация заключалась бы в том, чтобы использовать лучшее решение, адаптированное к программе, которую вы сейчас разрабатываете.
Ответ 4
Фокусировка на подтипировании, игнорирование проблем, связанных с классами, наследование, ООП и т.д. У нас есть идея, что подтипирование представляет собой отношение isa между типами. Например, типы A и B имеют разные операции, но если A isa B, мы можем использовать любую из B-операций на A.
OTOH, используя другое традиционное отношение, если C hasa B, то мы можем повторно использовать любую из операций B на C. Обычно языки позволяют писать один с более сильным синтаксисом a.opOnB вместо a.super.opOnB, как это было бы быть в случае композиции, cbopOnB
Проблема заключается в том, что во многих случаях существует более одного способа связать два типа. Например, Real может быть встроен в Complex, предполагая 0 на мнимой части, но Complex может быть встроен в Real, игнорируя воображаемую часть, поэтому оба они могут рассматриваться как подтипы другого, а подтипирование заставляет одно отношение рассматриваться как предпочтительное. Кроме того, существуют более возможные отношения (например, представление Complex как Real с использованием theta-компонента полярного представления).
В формальной терминологии мы обычно говорим о морфизме к таким отношениям между типами и существуют особые виды морфизмов для отношений с различными свойствами (например, изоморфизм, гомоморфизм).
В языке с подтипированием обычно существует гораздо больше сахара на отношениях isa и, учитывая много возможных вложений, мы склонны видеть ненужное трение всякий раз, когда мы используем неподтвержденное отношение. Если мы добавим наследование наследования, классы и ООП, проблема станет намного более заметной и беспорядочной.
Ответ 5
Мой ответ не отвечает, почему его избегают, но пытается дать еще один намек на то, почему его можно избежать.
Используя "классы типов", вы можете добавить абстракцию над существующими типами/классами без их модификации. Наследование используется для выражения того, что некоторые классы являются специализациями более абстрактного класса. Но с помощью классов классов вы можете взять любые существующие классы и выразить, что все они имеют общее свойство, например, они 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
Ответ 6
Я не знаю Scala, но я думаю, что мантра "предпочитает композицию над наследованием" применима для Scala точно так же, как это делает для каждого другого языка программирования OO (и подтипирование часто используется с тем же значением, что и 'наследование'). Здесь
Предпочитаете композицию над наследованием?
вы найдете дополнительную информацию.
Ответ 7
Я думаю, что многие программисты из Scala являются бывшими Java-программистами. Они привыкли думать в терминах объектно-ориентированного подтипирования, и они должны иметь возможность легко найти OO-подобное решение для большинства проблем. Но функциональное программирование - это новая парадигма для обнаружения, поэтому люди просят разные решения.
Ответ 8
Это - лучшая работа, которую я нашел по этому вопросу. Мотивационная цитата из статьи -
Мы утверждаем, что хотя некоторые из более простых аспектов объектно-ориентированных языков совместимый с ML, добавление полноценной объектной системы класса к ML приводит к чрезмерно сложной типа и относительно небольшой выразительный выигрыш.