Зачем избегать подтипов? - PullRequest
34 голосов
/ 01 сентября 2011

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

Ответы [ 8 ]

14 голосов
/ 02 декабря 2011

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

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

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

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

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

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

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

9 голосов
/ 01 сентября 2011

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

5 голосов
/ 01 сентября 2011

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

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

Как уже упоминалось в этом ответе PSE :

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

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

4 голосов
/ 09 ноября 2011

Сосредоточение внимания на подтипах, игнорирование вопросов, связанных с классами, наследованием, ООП и т. Д. У нас есть идея, что подтип представляет собой связь 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, и, учитывая множество возможных вложений, мы склонны видеть ненужные трения всякий раз, когда используем непривязанное отношение. Если мы добавим наследование, классы и ООП, проблема станет намного более заметной и беспорядочной.

2 голосов
/ 01 сентября 2011

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

Используя «классы типов», вы можете добавить абстракцию поверх существующих типов / классов без их изменения.Наследование используется для выражения того, что некоторые классы являются специализациями более абстрактного класса.Но с классами типов вы можете взять любые существующие классы и указать, что все они имеют общее свойство, например, они 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

1 голос
/ 01 сентября 2011

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

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

вы найдете еще немного информации.

0 голосов
/ 01 сентября 2014

Это - лучшая статья, которую я нашел по этому вопросу.Мотивирующая цитата из статьи -

Мы утверждаем, что хотя некоторые из более простых аспектов объектно-ориентированных языков совместимы с ML, добавление полноценной объектно-ориентированной системы классов в ML приводит кчрезмерно сложная система типов и сравнительно небольшой выразительный выигрыш

0 голосов
/ 01 сентября 2011

Я думаю, что многие программисты Scala - бывшие программисты на Java. Они привыкли думать в терминах объектно-ориентированного подтипирования, и они должны быть в состоянии легко найти ОО-подобное решение для большинства проблем. Но функциональное программирование - это новая парадигма, которую нужно открывать, поэтому люди спрашивают о других решениях.

...