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

Что особенного в Монадах?

Монада - это математическая структура, которая в значительной степени используется в (чистом) функциональном программировании, в основном в Haskell. Однако имеется много других математических структур, таких как, например, аппликативные функторы, сильные монады или моноиды. Некоторые из них более конкретные, некоторые более общие. Тем не менее, монады гораздо более популярны. Почему это?

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

Другим объяснением может быть то, что Haskell предоставляет синтаксис для monads (do-notation), но не для других структур, что означает, что программисты Haskell (и, следовательно, исследователи функционального программирования) интуитивно обращаются к монадам, где более общий или специфический (эффективный ) также будет работать.

4b9b3361

Ответ 1

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

(Кроме того, сомнительно, является ли IO даже монада. Имеют ли законы монады? Что означают законы для IO, т.е. что означает равенство? Обратите внимание на проблемная связь с государственной монадой.)

Ответ 2

Если тип m :: * -> * имеет экземпляр Monad, вы получаете полный набор функций Turing с типом a -> m b. Это фантастически полезное свойство. Вы получаете возможность абстрагировать различные потоки контроля Тьюринга от конкретных значений. Это минимальный шаблон композиции, который поддерживает абстрагирование любого потока управления для работы с типами, которые его поддерживают.

Сравните это с Applicative, например. Там вы получаете только композиционные рисунки с вычислительной мощностью, эквивалентными автоматическому отжиманию. Конечно, правда, что больше типов поддерживают композицию с более ограниченной мощностью. И это правда, что когда вы ограничиваете доступную мощность, вы можете делать дополнительные оптимизации. Эти две причины объясняют, почему класс Applicative существует и полезен. Но вещи, которые могут быть экземплярами Monad, обычно, так что пользователи типа могут выполнять наиболее общие операции, доступные с типом.

Edit: По многочисленным просьбам, вот некоторые функции, использующие класс Monad:

ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM c x y = c >>= \z -> if z then x else y

whileM :: Monad m => (a -> m Bool) -> (a -> m a) -> a -> m a
whileM p step x = ifM (p x) (step x >>= whileM p step) (return x)

(*&&) :: Monad m => m Bool -> m Bool -> m Bool
x *&& y = ifM x y (return False)

(*||) :: Monad m => m Bool -> m Bool -> m Bool
x *|| y = ifM x (return True) y

notM :: Monad m => m Bool -> m Bool
notM x = x >>= return . not

Объединение тех, у кого есть синтаксис do (или raw >>=), дает вам привязку имени, неопределенный цикл и полную логическую логику. Это хорошо известный набор примитивов, достаточный для обеспечения полноты Тьюринга. Обратите внимание, как все функции были сняты, чтобы работать над монадическими значениями, а не с простыми значениями. Все монадические эффекты связаны только тогда, когда это необходимо - только эффекты от выбранной ветки ifM связаны с его окончательным значением. Оба *&& и *|| игнорируют свой второй аргумент, когда это возможно. И так далее.

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

Теперь посмотрим, что происходит с этими функциями с интерфейсом Applicative.

ifA :: Applicative f => f Bool -> f a -> f a -> f a
ifA c x y = (\c' x' y' -> if c' then x' else y') <$> c <*> x <*> y

Ну, мм. Он получил подпись того же типа. Но здесь действительно большая проблема. Эффекты как x, так и y связаны в составной структуре, независимо от того, какое из них выбрано.

whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <$> step x) (pure x)

Хорошо, хорошо, похоже, было бы хорошо, за исключением того факта, что это бесконечный цикл, потому что ifA всегда будет выполнять обе ветки... Кроме того, это даже не так близко. pure x имеет тип f a. whileA p step <$> step x имеет тип f (f a). Это даже не бесконечный цикл. Это ошибка компиляции. Попробуйте еще раз.

whileA :: Applicative f => (a -> f Bool) -> (a -> f a) -> a -> f a
whileA p step x = ifA (p x) (whileA p step <*> step x) (pure x)

Хорошо стреляй. Даже не заходите так далеко. whileA p step имеет тип a -> f a. Если вы попытаетесь использовать его в качестве первого аргумента для <*>, он захватывает экземпляр Applicative для конструктора верхнего типа, который (->), а не f. Да, это тоже не сработает.

Фактически, единственная функция из моих Monad примеров, которые будут работать с интерфейсом Applicative, это notM. Эта конкретная функция отлично работает только с интерфейсом Functor. Отдых? Они терпят неудачу.

Конечно, следует ожидать, что вы можете писать код, используя интерфейс Monad, который вы не можете использовать с интерфейсом Applicative. В конце концов, это более строго. Но что интересно, что вы теряете. Вы теряете способность составлять функции, которые изменяют, какие эффекты они имеют на основе их ввода. То есть вы теряете способность писать определенные шаблоны управления, которые составляют функции с типами a -> f b.

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

У многих других типов, кроме IO, есть семантически допустимые интерфейсы Monad. И бывает, что у Haskell есть языковые возможности для абстракции по всему интерфейсу. Из-за этих факторов Monad является ценным классом для предоставления экземпляров, когда это возможно. Это дает вам доступ ко всем существующим абстрактным функциям, предусмотренным для работы с монадическими типами, независимо от того, что представляет собой конкретный тип.

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

Ответ 3

Во-первых, я думаю, что не совсем верно, что монады гораздо более популярны, чем что-либо еще; как Functor, так и Monoid имеют много примеров, которые не являются монадами. Но они оба очень специфичны; Functor обеспечивает отображение, объединение моноидов. Аппликативный - это тот класс, о котором я могу думать, что, вероятно, недостаточно используется, учитывая его значительную силу, в основном благодаря тому, что он является относительно недавним дополнением к языку.

Но да, монады чрезвычайно популярны. Частично это обозначение; много моноидов предоставляют экземпляры Monad, которые просто добавляют значения к работающему аккумулятору (по сути, неявный писатель). Хорошим примером является библиотека blaze-html. Причина, по-моему, заключается в силе сигнатуры типа (>>=) :: Monad m => m a -> (a -> m b) -> m b. Хотя fmap и mappend полезны, то, что они могут сделать, довольно узко ограничено. Однако связывание может выражать самые разные вещи. Это, конечно, канонизировано в монаде IO, возможно, лучший чистый функциональный подход к IO перед потоками и FRP (и все же полезный для них для простых задач и определения компонентов). Но он также обеспечивает неявное состояние (Reader/Writer/ST), что позволяет избежать очень утомительной передачи переменной. Различные государственные монады, в частности, важны, потому что они обеспечивают гарантию того, что состояние однопоточное, что позволяет превращать структуры в чистый (не-IO) код перед слиянием. Но bind имеет несколько более экзотических применений, таких как сглаживание вложенных структур данных (монады List и Set), оба из которых весьма полезны на их месте (и я обычно вижу, что они используются desugared, вызывая liftM или ( → =) явно, так что это не вопрос обозначения). Таким образом, в то время как Functor и Monoid (и несколько более редкие Foldable, Alternative, Traversable и другие) обеспечивают стандартизованный интерфейс для довольно простой функции, привязка Monad значительно более гибкая.

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

Ответ 4

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

Есть положительная и отрицательная сторона: положительно, что вы можете выразить все те структуры управления, которые вы знаете из императивного программирования, и целую кучу из них вы этого не делаете. Недавно я разработал монаду, которая позволяет вам повторно посещать вычисление где-то посередине со слегка измененным контекстом. Таким образом, вы можете выполнить вычисление, и если это не удается, попробуйте еще раз со слегка скорректированными значениями. Кроме того, монадические действия являются первоклассными и что вы создаете такие вещи, как циклы или обработку исключений. Хотя while является примитивным в C в Haskell, это фактически просто регулярная функция.

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

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

Ответ 5

Монады являются особенными из-за нотации do, которая позволяет писать императивные программы на функциональном языке. Monad - это абстракция, которая позволяет объединять императивные программы от небольших, многоразовых компонентов (которые сами по себе являются обязательными программами). Трансформаторы Monad являются особенными, потому что они представляют собой усовершенствование языка с новыми функциями.