Как смоделировать иерархии классов в Haskell?

Я - разработчик C#. Происходя из стороны OO мира, я запускаю с размышления с точки зрения интерфейсов, классов и ввожу иерархии. Из-за отсутствия OO в Haskell иногда я нахожу меня застрявшим, и я не могу думать о способе смоделировать определенные проблемы с Haskell.

Как смоделировать, в Haskell, ситуации с реальным миром, включающие иерархии классов такой как один показанный здесь: http://www.braindelay.com/danielbray/endangered-object-oriented-programming/isHierarchy-4.gif

16
задан Kibarim 25 February 2016 в 16:47
поделиться

3 ответа

Предположим, что следующие операции: люди могут говорить, собаки могут лаять, и все представители вида могут спариваться с представителями того же вида, если они имеют противоположный пол. Я бы определил это в haskell так:

data Gender = Male | Female deriving Eq

class Species s where
    gender :: s -> Gender

-- Returns true if s1 and s2 can conceive offspring
matable :: Species a => a -> a -> Bool
matable s1 s2 = gender s1 /= gender s2

data Human = Man | Woman
data Canine = Dog | Bitch

instance Species Human where
    gender Man = Male
    gender Woman = Female

instance Species Canine where
    gender Dog = Male
    gender Bitch = Female

bark Dog = "woof"
bark Bitch = "wow"

speak Man s = "The man says " ++ s
speak Woman s = "The woman says " ++ s

Теперь операция matable имеет тип Species s => s -> s -> Bool , bark имеет type Canine -> String и speak имеет тип Human -> String -> String .

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

Редактировать: В ответ на комментарий Даниэля:

Простая иерархия для коллекций может выглядеть так (игнорируя уже существующие классы, такие как Foldable и Functor):

class Foldable f where
    fold :: (a -> b -> a) -> a -> f b -> a

class Foldable m => Collection m where
    cmap :: (a -> b) -> m a -> m b
    cfilter :: (a -> Bool) -> m a -> m a

class Indexable i where
    atIndex :: i a -> Int -> a

instance Foldable [] where
    fold = foldl

instance Collection [] where
    cmap = map
    cfilter = filter

instance Indexable [] where
    atIndex = (!!)

sumOfEvenElements :: (Integral a, Collection c) => c a -> a
sumOfEvenElements c = fold (+) 0 (cfilter even c)

Теперь sumOfEvenElements принимает любую коллекцию интегралов и возвращает сумму всех четных элементов этой коллекции.

13
ответ дан 30 November 2019 в 15:33
поделиться

Вместо классов и объектов Haskell использует абстрактные типы данных . Это действительно два совместимых взгляда на проблему организации способов построения и наблюдения информации. Лучшее пособие, которое я знаю по этому поводу, - это эссе Уильяма Кука Объектно-ориентированное программирование против абстрактных типов данных . У него есть несколько очень четких объяснений того, что

  • В системе, основанной на классах, код организован вокруг различных способов построения абстракций. Обычно каждому способу построения абстракции присваивается собственный класс. Методы умеют наблюдать только за свойствами этой конструкции.

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

Статья Кука покажет вам красивую матричную компоновку абстракций и научит, как организовать любой класс как ADY или наоборот.

Иерархии классов включают еще один элемент: повторное использование реализаций посредством наследования.В Haskell такое повторное использование вместо этого достигается с помощью функций первого класса: функция в абстракции Primate является значением, а реализация абстракции Human может повторно использовать любые функции из ] Primate , может обернуть их, чтобы изменить их результаты, и так далее.

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


Дополнение: также можно использовать систему типов и классов Haskell, чтобы попытаться имитировать иерархию классов, но это уже совсем другое дело. Классы типов достаточно похожи на обычные классы, чтобы работать со многими стандартными примерами, но они достаточно разные, так что могут быть очень большие сюрпризы и несоответствия. Хотя классы типов являются бесценным инструментом для программиста на Haskell, я бы рекомендовал всем, кто изучает Haskell, научиться разрабатывать программы с использованием абстрактных типов данных.

6
ответ дан 30 November 2019 в 15:33
поделиться

Прежде всего: стандартный объектно-ориентированный дизайн не будет хорошо работать в Haskell. Вы можете бороться с языком и попытаться создать что-то подобное, но это будет упражнением в разочаровании. Итак, первый шаг - это поиск решений вашей проблемы в стиле Haskell вместо поиска способов написания решения в стиле ООП на Haskell .

Но это легче сказать, чем сделать! С чего начать?

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

  • Объекты : грубо говоря, объект - это комбинация некоторых данных с методами, работающими с этими данными. В Haskell данные обычно структурируются с использованием алгебраических типов данных ; методы можно рассматривать как функции, принимающие данные объекта в качестве начального, неявного аргумента .
  • Инкапсуляция : Однако возможность проверки данных объекта обычно ограничивается его собственными методами. В Haskell есть различные способы скрыть часть данных, два примера:
    • Определите тип данных в отдельном модуле, который не экспортирует конструкторы типа . Только функции в этом модуле могут проверять или создавать значения этого типа. Это в некоторой степени сопоставимо с защищенными или внутренними членами.
    • Использовать частичное приложение . Рассмотрим функцию map с перевернутыми аргументами. Если вы примените его к списку Int s, вы получите функцию типа (Int -> b) -> [b] .Список, который вы ему дали, в некотором смысле все еще «там», но ничто другое не может использовать его, кроме как через функцию. Это сравнимо с частными членами, а частично применяемая исходная функция сравнима с конструктором в стиле ООП.
  • "Ad-hoc" полиморфизм : Часто в объектно-ориентированном программировании нас заботит только то, что что-то реализует метод; когда мы вызываем его, конкретный вызываемый метод определяется на основе фактического типа. Haskell предоставляет классов типов для перегрузки функций во время компиляции, которые во многих отношениях более гибки, чем те, что можно найти в языках ООП.
  • Повторное использование кода : Честно говоря, я считаю, что повторное использование кода через наследование было и остается ошибкой. Подмешивания, которые можно найти в чем-то вроде Ruby, кажутся мне лучшим объектно-ориентированным решением. В любом случае, в любом функциональном языке стандартный подход состоит в том, чтобы исключить общее поведение с помощью функций высшего порядка, а затем специализировать форму общего назначения. Классическим примером здесь являются функции свертки , которые обобщают почти все итерационные циклы, преобразования списков и линейно рекурсивные функции.
  • Интерфейсы : в зависимости от того, как вы используете интерфейс, есть разные варианты:
    • Разделение реализации : Полиморфные функции с ограничениями класса типов - это то, что вам здесь нужно. Например, функция sort имеет тип (Ord a) => [a] -> [a] ; он полностью отделен от деталей типа, который вы ему даете, кроме того, что это должен быть список какого-то типа, реализующего Ord .
    • Работа с несколькими типами через общий интерфейс : для этого вам понадобится либо языковое расширение для экзистенциальных типов , либо, чтобы не усложнять задачу, используйте некоторые вариации на частичном приложении , как указано выше - вместо значений и функций вы можете применять к ним, применять функции заранее и работать с результатами.
  • Подтип , также известный как отношение «есть-а»: именно здесь вам в основном не везет. Но, исходя из опыта, проработанного профессиональным разработчиком C # в течение многих лет, случаи, когда вам действительно требуется подтипы, не очень распространены. Вместо этого подумайте о вышеперечисленном и о том, какое поведение вы пытаетесь уловить с помощью отношений подтипов.

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

В качестве последнего добавления, как программист на C #, вам может быть интересно исследовать связи между ним и Haskell. Немало людей, ответственных за C #, также являются программистами на Haskell, и некоторые недавние дополнения к C # были во многом основаны на Haskell. Наиболее примечательной является, вероятно, монадическая структура, лежащая в основе LINQ, при этом IEnumerable по сути является монадой списка.

32
ответ дан 30 November 2019 в 15:33
поделиться