Являются ли монады жизнеспособной (возможно, предпочтительной) альтернативой иерархии наследования?

20

Я собираюсь использовать описание монад, не зависящее от языка, например, сначала для описания моноидов:

Моноид это (примерно) набор функций , которые принимают некоторый тип в качестве параметра и возвращают один и тот же тип.

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

Обратите внимание, что это описания, а не определения. Не стесняйтесь атаковать это описание!

Таким образом, на языке ОО монада допускает такие операции:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

Обратите внимание, что монада определяет и контролирует семантику этих операций, а не содержащийся класс.

Традиционно, в ОО-языке мы использовали бы иерархию классов и наследование, чтобы обеспечить эту семантику. Таким образом , мы будем иметь Birdкласс с методами takeOff(), flyAround()и land(), и утка унаследует те.

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

Кроме того, как только мы говорим, что Penguin - это Bird, мы сталкиваемся с проблемами множественного наследования, например, если у нас также есть иерархия Swimmer.

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

Так что в этом случае у нас будет Flier<T>монада, как в примере выше:

Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()

... и мы никогда не будем создавать экземпляр Flier<Penguin>. Мы могли бы даже использовать статическую типизацию, чтобы предотвратить это, возможно, с помощью интерфейса маркера. Или проверка возможностей во время выполнения, чтобы выручить. Но на самом деле программист никогда не должен помещать пингвина во Флайера, в том же смысле, что они никогда не должны делиться на ноль.

Кроме того, это более широко применимо. Летчик не должен быть птицей. Например Flier<Pterodactyl>, или Flier<Squirrel>без изменения семантики этих отдельных типов.

Как только мы классифицируем семантику с помощью компонуемых функций в контейнере, а не с иерархиями типов, это решает старые проблемы с классами, которые «типа делают, типа не» вписываются в определенную иерархию. Это также легко и понятно допускает множественную семантику для класса, Flier<Duck>как и Swimmer<Duck>. Кажется, что мы боролись с несоответствием импеданса, классифицируя поведение с иерархиями классов. Монады справляются с этим элегантно.

Итак, мой вопрос, так же, как мы стали отдавать предпочтение композиции, а не наследованию, имеет ли смысл отдавать предпочтение монадам, а не наследованию?

(Кстати, я не был уверен, должно ли это быть здесь или в Comp Sci, но это больше похоже на практическую проблему моделирования. Но, может быть, там лучше).

обкрадывать
источник
1
Не уверен, что понимаю, как это работает: белка и утка летят не одинаково - поэтому в этих классах нужно реализовать «действие мухи» ... И летчику нужен метод, чтобы сделать белку и утку летать ... Может быть, в обычном интерфейсе Flier ... Ой, подождите ... Я что-то пропустил?
assylias
Интерфейсы отличаются от наследования классов, потому что интерфейсы определяют возможности, а функциональное наследование определяет реальное поведение. Даже в «композиции по наследованию» определение интерфейсов все еще является важным механизмом (например, полиморфизм). Интерфейсы не сталкиваются с одинаковыми проблемами множественного наследования. Кроме того, каждый летчик может предоставить (через интерфейс и полиморфизм) свойства возможности, такие как «getFlightSpeed ​​()» или «getManuverability ()» для контейнера, который будет использоваться.
Роб
3
Вы пытаетесь спросить, всегда ли использование параметрического полиморфизма является жизнеспособной альтернативой полиморфизму подтипа?
ChaosPandion
да, со складкой добавления компонуемых функций, которые сохраняют семантику. Параметризованные типы контейнеров существуют уже давно, но сами по себе они не кажутся мне полным ответом. Вот почему мне интересно, может ли образец монады играть более фундаментальную роль?
Роб
6
Я не понимаю вашего описания моноидов и монад. Ключевое свойство моноидов заключается в том, что он включает в себя ассоциативную двоичную операцию (например, сложение с плавающей запятой, целочисленное умножение или конкатенацию строк). Монада - это абстракция, которая поддерживает последовательность различных (возможно зависимых) вычислений в некотором порядке.
Rufflewind

Ответы:

15

Короткий ответ - нет , монады не являются альтернативой иерархии наследования (также известный как полиморфизм подтипов). Похоже, вы описываете параметрический полиморфизм , который используют монады, но не единственное, что делает это.

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

  1. Они могут быть использованы синергетически по крайней мере в двух смыслах:
    • проверьте Typeclassopedia , которая охватывает многие классы типов Haskell. Вы заметите, что между ними существуют наследственные отношения. Например, Monad происходит от Applicative, который сам происходит от Functor.
    • типы данных, которые являются экземплярами монад, могут участвовать в иерархиях классов. Помните, что Monad больше похож на интерфейс - его реализация для заданного типа говорит вам кое-что о типе данных, но не обо всем.
  2. Попытка использовать одну для другой будет трудной и безобразной.

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


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

  1. Монада - это набор функций, которые принимают тип контейнера в качестве параметра и возвращают тот же тип контейнера.

    Нет, это есть Monadв Haskell: параметризованный типа m aс реализацией return :: a -> m aи (>>=) :: m a -> (a -> m b) -> m b, удовлетворяющей следующие законами:

    return a >>= k  ==  k a
    m >>= return  ==  m
    m >>= (\x -> k x >>= h)  ==  (m >>= k) >>= h
    

    Есть несколько экземпляров Monad, которые не являются контейнерами ( (->) b), и есть некоторые контейнеры, которые не являются (и не могут быть созданы) экземплярами Monad ( Setиз-за ограничения класса типа). Так что «контейнерная» интуиция плохая. Смотрите это для большего количества примеров.

  2. Таким образом, на языке ОО монада допускает такие операции:

      Flier<Duck> m = new Flier<Duck>(duck).takeOff().flyAround().land()
    

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

    Flier<Duck> m = land(flyAround(takeOff(new Flier<Duck>(duck))));
    

    Я считаю, что это шаблон, известный как «свободный интерфейс» или «цепочка методов» (но я не уверен).

  3. Обратите внимание, что монада определяет и контролирует семантику этих операций, а не содержащийся класс.

    Типы данных, которые также являются монадами, могут (и почти всегда делают!) Иметь операции, не связанные с монадами. Вот пример Haskell, состоящий из трех функций, []которые не имеют ничего общего с монадами: []«определяет и контролирует семантику операции», а «содержащийся класс» - нет, но этого недостаточно для создания монады:

    \predicate -> length . filter predicate . reverse
    
  4. Вы правильно заметили, что существуют проблемы с использованием иерархий классов для моделирования вещей. Тем не менее, ваши примеры не дают никаких доказательств того, что монады могут:

    • Делать хорошую работу в том, что наследство хорошо
    • Делать хорошую работу в том, что наследство плохо
Сообщество
источник
3
Спасибо! Много для меня, чтобы обработать. Я не чувствую себя плохо - я очень ценю понимание. Мне было бы хуже носить с собой плохие идеи. :) (Переходит к сути стека обмена!)
Роб
1
@RobY Добро пожаловать! Кстати, если вы не слышали об этом раньше, я рекомендую LYAH, так как это отличный источник для изучения монад (и Haskell!), Потому что у него есть множество примеров (и я считаю, что делать множество примеров - лучший способ решать монады).
Здесь много всего; Я не хочу заваливать комментарии, но несколько комментариев: # 2 land(flyAround(takeOff(new Flier<Duck>(duck))))не работает (по крайней мере, в ОО), потому что эта конструкция требует нарушения инкапсуляции, чтобы добраться до деталей Flier. Приковывая цепочки к классу, детали Flier остаются скрытыми, и он может сохранить свою семантику. Это похоже на причину, по которой в Хаскеле связывается монада, (a, M b)а не (M a, M b)так, что монаде не нужно выставлять свое состояние для функции «действие».
Роб
# 1, к сожалению, я пытаюсь стереть строгое определение Monad в Haskell, потому что отображение чего-либо в Haskell имеет большую проблему: композицию функций, включая композицию на конструкторах , которую вы не можете легко сделать в пешеходном языке, таком как Java. Таким образом, он unitстановится (главным образом) конструктором для содержащегося типа и bindстановится (главным образом) подразумеваемой операцией времени компиляции (т.е. раннего связывания), которая связывает функции «действия» с классом. Если у вас есть первоклассные функции или класс Function <A, Monad <B >>, то bindметод может выполнить позднюю привязку, но я расскажу об этом злоупотреблении далее. ;)
Роб
№ 3 согласен, и в этом вся прелесть. Если Flier<Thing>контролирует семантику полета, то он может предоставлять множество данных и операций, которые поддерживают семантику полета, в то время как семантика, специфичная для «монады», на самом деле просто делает ее цепной и инкапсулированной. Эти проблемы могут не Resource<String>относиться (и с теми, которые я использовал, не) к классу внутри монады: например, имеет свойство httpStatus, а String - нет.
Роб
1

Итак, мой вопрос, так же, как мы стали отдавать предпочтение композиции, а не наследованию, имеет ли смысл отдавать предпочтение монадам, а не наследованию?

На не-ОО языках, да. В более традиционных языках ОО я бы сказал нет.

Проблема в том, что большинство языков не имеют специализации типов, то есть вы не можете создавать Flier<Squirrel>иFlier<Bird> иметь разные реализации. Вы должны сделать что-то вроде static Flier Flier::Create(Squirrel)(и затем перегрузить для каждого типа). Это, в свою очередь, означает, что вы должны изменять этот тип всякий раз, когда добавляете новое животное, и, вероятно, дублировать немало кода, чтобы он работал.

Да, и не на нескольких языках (например, C #) public class Flier<T> : T {}это незаконно. Это даже не построить. Большинство, если не все ОО-программисты будут ожидать, Flier<Bird>что они все еще будут Bird.

Telastyn
источник
Спасибо за комментарий. У меня есть еще несколько мыслей, но просто тривиально, хотя Flier<Bird>это параметризованный контейнер, никто не посчитает, что это Bird(!?) List<String>Это List, а не String.
Роб
@RobY - Flierэто не просто контейнер. Если вы считаете, что это просто контейнер, почему вы думаете, что он может заменить использование наследования?
Теластин
Я потерял тебя там ... моя точка зрения в том, что монада - это расширенный контейнер. Animal / Bird / Penguinобычно плохой пример, потому что он вводит все виды семантики. Практическим примером является монада REST-ish, которую мы используем: Resource<String>.from(uri).get() Resourceдобавляет семантику поверх String(или некоторого другого типа), так что это явно не a String.
Роб
@RobY - но это также никак не связано с наследованием.
Теластин
За исключением того, что это другой вид сдерживания. Я могу поместить String в Resource или абстрагировать класс ResourceString и использовать наследование. Я думаю, что помещение класса в контейнер цепочки - лучший способ абстрагировать поведение, чем помещать его в иерархию классов с наследованием. Так что «никак не связано» в смысле «замена / устранение» - да.
Роб