Я собираюсь использовать описание монад, не зависящее от языка, например, сначала для описания моноидов:
Моноид это (примерно) набор функций , которые принимают некоторый тип в качестве параметра и возвращают один и тот же тип.
Монада есть (примерно) набор функций , которые принимают обертки типа в качестве параметра и возвращает один и тот же тип обертки.
Обратите внимание, что это описания, а не определения. Не стесняйтесь атаковать это описание!
Таким образом, на языке ОО монада допускает такие операции:
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, но это больше похоже на практическую проблему моделирования. Но, может быть, там лучше).
источник
Ответы:
Короткий ответ - нет , монады не являются альтернативой иерархии наследования (также известный как полиморфизм подтипов). Похоже, вы описываете параметрический полиморфизм , который используют монады, но не единственное, что делает это.
Насколько я понимаю, монады по сути не имеют ничего общего с наследованием. Я бы сказал, что эти две вещи более или менее ортогональны: они предназначены для решения различных проблем и так:
Наконец, хотя это касается вашего вопроса, вам может быть интересно узнать, что у монад есть невероятно мощные способы сочинения; прочитайте о монадных трансформаторах, чтобы узнать больше. Тем не менее, это все еще активная область исследований, потому что мы (и мы, я имею в виду людей в 100000 раз умнее меня) не нашли хороших способов составления монад, и кажется, что некоторые монады не сочиняют произвольно.
Теперь, чтобы точно ответить на ваш вопрос (извините, я намерен, чтобы это помогло, а не заставляло вас чувствовать себя плохо): я чувствую, что есть много сомнительных предпосылок, на которые я попытаюсь пролить свет.
Нет, это есть
Monad
в Haskell: параметризованный типаm a
с реализациейreturn :: a -> m a
и(>>=) :: m a -> (a -> m b) -> m b
, удовлетворяющей следующие законами:Есть несколько экземпляров Monad, которые не являются контейнерами (
(->) b
), и есть некоторые контейнеры, которые не являются (и не могут быть созданы) экземплярами Monad (Set
из-за ограничения класса типа). Так что «контейнерная» интуиция плохая. Смотрите это для большего количества примеров.Нет, совсем нет. Этот пример не требует монады. Все, что для этого требуется, - это функции с соответствующими типами ввода и вывода. Вот еще один способ написать его, который подчеркивает, что это просто функциональное приложение:
Я считаю, что это шаблон, известный как «свободный интерфейс» или «цепочка методов» (но я не уверен).
Типы данных, которые также являются монадами, могут (и почти всегда делают!) Иметь операции, не связанные с монадами. Вот пример Haskell, состоящий из трех функций,
[]
которые не имеют ничего общего с монадами:[]
«определяет и контролирует семантику операции», а «содержащийся класс» - нет, но этого недостаточно для создания монады:Вы правильно заметили, что существуют проблемы с использованием иерархий классов для моделирования вещей. Тем не менее, ваши примеры не дают никаких доказательств того, что монады могут:
источник
land(flyAround(takeOff(new Flier<Duck>(duck))))
не работает (по крайней мере, в ОО), потому что эта конструкция требует нарушения инкапсуляции, чтобы добраться до деталей Flier. Приковывая цепочки к классу, детали Flier остаются скрытыми, и он может сохранить свою семантику. Это похоже на причину, по которой в Хаскеле связывается монада,(a, M b)
а не(M a, M b)
так, что монаде не нужно выставлять свое состояние для функции «действие».unit
становится (главным образом) конструктором для содержащегося типа иbind
становится (главным образом) подразумеваемой операцией времени компиляции (т.е. раннего связывания), которая связывает функции «действия» с классом. Если у вас есть первоклассные функции или класс Function <A, Monad <B >>, тоbind
метод может выполнить позднюю привязку, но я расскажу об этом злоупотреблении далее. ;)Flier<Thing>
контролирует семантику полета, то он может предоставлять множество данных и операций, которые поддерживают семантику полета, в то время как семантика, специфичная для «монады», на самом деле просто делает ее цепной и инкапсулированной. Эти проблемы могут неResource<String>
относиться (и с теми, которые я использовал, не) к классу внутри монады: например, имеет свойство httpStatus, а String - нет.На не-ОО языках, да. В более традиционных языках ОО я бы сказал нет.
Проблема в том, что большинство языков не имеют специализации типов, то есть вы не можете создавать
Flier<Squirrel>
иFlier<Bird>
иметь разные реализации. Вы должны сделать что-то вродеstatic Flier Flier::Create(Squirrel)
(и затем перегрузить для каждого типа). Это, в свою очередь, означает, что вы должны изменять этот тип всякий раз, когда добавляете новое животное, и, вероятно, дублировать немало кода, чтобы он работал.Да, и не на нескольких языках (например, C #)
public class Flier<T> : T {}
это незаконно. Это даже не построить. Большинство, если не все ОО-программисты будут ожидать,Flier<Bird>
что они все еще будутBird
.источник
Flier<Bird>
это параметризованный контейнер, никто не посчитает, что этоBird
(!?)List<String>
Это List, а не String.Flier
это не просто контейнер. Если вы считаете, что это просто контейнер, почему вы думаете, что он может заменить использование наследования?Animal / Bird / Penguin
обычно плохой пример, потому что он вводит все виды семантики. Практическим примером является монада REST-ish, которую мы используем:Resource<String>.from(uri).get()
Resource
добавляет семантику поверхString
(или некоторого другого типа), так что это явно не aString
.