Ответ 1
Вы можете использовать типы сумм для представления всей иерархии без потери структуры. Что-то вроде этого сделало бы это:
data Shape = IsPoint Point
| IsLine Line
| IsPolygon Polygon
data Point = Point { x :: Int, y :: Int }
data Line = Line { a :: Point, b :: Point }
data Polygon = IsTriangle Triangle
| IsQuad Quad
| ...
И так далее. Основной шаблон состоит в том, что вы переводите каждый абстрактный класс OO в тип суммы Haskell, причем каждый из его непосредственных подклассов OO (которые могут быть абстрактными) в качестве вариантов типа суммы. Конкретные классы - это типы продуктов/записей с фактическими элементами данных в них. 1
То, что вы теряете по сравнению с ООП, к которому вы привыкли, моделируя вещи таким образом, - это не способность представлять вашу иерархию, а способность расширять ее, не касаясь существующего кода. Типы сумм "закрыты", где наследование ОО "открыто". Если позже вы решите, что хотите Circle
для Shape
, вы должны добавить его в Shape
, а затем добавить для него случаи, когда вы сопоставляете шаблоны с Shape
.
Однако такая иерархия, вероятно, требует довольно либерального понижения в OO. Например, если вам нужна функция, которая может определить, пересекаются ли две формы, что, вероятно, является абстрактным методом на Shape
, например Shape.intersects(Shape other)
, поэтому каждый подтип получает возможность писать свою собственную реализацию. Но когда я пишу Rectangle.intersects(Shape other)
, это в принципе невозможно в общем, не зная, какие другие подклассы Shape
находятся там. Мне нужно будет использовать isinstance
проверки, чтобы увидеть, что на самом деле other
. Но это на самом деле означает, что я, вероятно, не могу просто добавить мой новый подкласс Circle
, не пересматривая существующий код; иерархия OO, где необходимы проверки состояния, де-факто так же "закрыта", как иерархия типов сумм Haskell. В основном сопоставление шаблонов на одном из типов сумм, генерируемых при применении этого шаблона, эквивалентно isinstancing и downcasting в версии OO. Только потому, что типы сумм исчерпывающе известны компилятору (возможно только , потому что они закрыты), если я добавлю случай Circle
к Shape
, компилятор сможет рассказать мне обо всех места, которые мне нужно пересмотреть, чтобы обработать этот случай. 2
Если у вас есть иерархия, которая не нуждается в большом понижении, это означает, что у разных базовых классов есть существенные и полезные интерфейсы, которые они гарантируют быть доступными, и вы обычно используете вещи через этот интерфейс, а не включаете то, что возможно, это возможно, тогда вы, вероятно, можете использовать классы типов. Вам все еще нужны все "листовые" типы данных (типы продуктов с фактическими полями данных), только вместо того, чтобы добавлять оболочки типа суммы, чтобы сгруппировать их, вы добавляете классы типов для общего интерфейса. Если вы можете использовать этот стиль перевода, вы можете легко добавить новые случаи (просто добавьте новый тип данных Circle
и экземпляр, чтобы сказать, как он реализует класс типа Shape
; все места, которые являются полиморфными в любой тип в классе Shape
теперь будет обрабатывать Circle
). Но если вы делаете это в OO, у вас всегда есть понижение, доступное как побег-люк, когда оказывается, что вы не можете обрабатывать фигуры в целом; с этой конструкцией в Haskell это невозможно. 3
Но мой "реальный" ответ на "как я представляю иерархии типов OO в Haskell", к сожалению, является банальным: я этого не делаю. Я проектирую по-разному в Haskell, чем в языках OO 4 и на практике это просто не огромная проблема. Но, чтобы сказать, как я буду дизайн этого случая по-другому, мне нужно будет узнать больше о том, для чего вы их используете. Например, вы можете сделать что-то вроде представления формы как функции Point -> Bool
(которая говорит вам, присутствует ли какая-либо данная точка внутри формы) и имеет такие вещи, как circle :: Point -> Int -> (Point -> Bool)
для генерации таких функций, соответствующих нормальным формам; это представление является удивительным для формирования составных форм пересечения/объединения, ничего не зная о них (intersect shapeA shapeB = \point -> shapeA point && shapeB point
), но ужасно для вычисления таких вещей, как области и окружности.
1 Если у вас есть абстрактные классы с членами данных или у вас есть конкретные классы, которые также имеют дополнительные подклассы, вы можете вручную подтолкнуть данные к "листьям", разделить унаследованные элементы данных в общую запись и заставить все "листья" содержать один из них, разбивать слой так, чтобы у вас был тип продукта, содержащий унаследованные элементы данных и тип суммы (где этот тип суммы затем "делится" на опции для подклассы), такие вещи.
2 Если вы используете шаблоны catch-all, это предупреждение может быть не исчерпывающим, поэтому оно не всегда является доказательством пули, но насколько доказательством этого является то, как вы кодируете.
3Если вы не выбираете информацию типа времени выполнения с таким решением, как Typeable
, но это не невидимое изменение; ваши звонящие также должны выбрать в нем.
4 На самом деле я, вероятно, не буду создавать такую иерархию, как это даже в языках OO. Я считаю, что это не так полезно, как вы думаете в реальных программах, поэтому совет "предпочтение композиции над наследованием".