Подтвердить что ты не робот

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

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

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

4b9b3361

Ответ 1

Предположим следующие действия: люди могут говорить, собаки могут лаять, и все члены вида могут спариваться с членами одного и того же вида, если они имеют противоположный пол. Я бы определил это в 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 имеет тип Canine -> String и speak имеет тип Human -> String -> String.

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

Изменить: В ответ на комментарий Daniel:

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

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

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

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


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

Ответ 4

Haskell - мой любимый язык, это чистый функциональный язык. У него нет побочных эффектов, нет назначения. Если вам трудно перейти на этот язык, возможно, F # - лучшее место для начала функционального программирования. F # не является чистым.

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

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

Объявление данных позволяет вам определять структуры данных, объединяющие продукты (эквивалентные структуре на языке C) и союзы (эквивалентные объединению в C), получающая часть o декларация позволяет наследовать методы по умолчанию.

Тип данных (структура данных) относится к классу, если имеет реализацию набора методов в классе. Например, если вы можете определить метод show:: a → String для своего типа данных, то он относится к классу Show, вы можете определить свой тип данных как экземпляр класса Show.

Это отличается от использования класса в некоторых языках OO, где он используется как способ определения структур + методов.

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

Абстракция поддерживается в Haskell, ее очень легко объявить. Например, этот код с сайта Haskell:

data Tree a = Nil 
            | Node { left  :: Tree a,
                     value :: a,
                     right :: Tree a }

объявляет селектора left, value, right. конструкторы могут быть определены следующим образом, если вы хотите добавить их в список экспорта в объявлении модуля:

node = Node 
nil = Nil

Модули строятся так же, как в Modula. Вот еще один пример из того же сайта:

module Stack (Stack, empty, isEmpty, push, top, pop) where

empty :: Stack a
isEmpty :: Stack a -> Bool
push :: a -> Stack a -> Stack a
top :: Stack a -> a
pop :: Stack a -> (a,Stack a)

newtype Stack a = StackImpl [a] -- opaque!
empty = StackImpl []
isEmpty (StackImpl s) = null s
push x (StackImpl s) = StackImpl (x:s)
top (StackImpl s) = head s
pop (StackImpl (s:ss)) = (s,StackImpl ss)

В этом вопросе есть что сказать, надеюсь, этот комментарий поможет!