Как вы определяете монадические шаблоны дизайна? - PullRequest
41 голосов
/ 08 января 2012

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

Итак, как вы определяетепроблема как монадическая в природе?Каковы хорошие шаблоны проектирования для монадического дизайна?Как вы подходите, когда понимаете, что некоторый код будет лучше преобразован в монаду?

Ответы [ 5 ]

58 голосов
/ 08 января 2012

Полезное правило - когда вы видите значения в контексте ;Монады можно рассматривать как наслоение «эффектов» на:

  • Может быть: частичное (использует: вычисления, которые могут дать сбой)
  • Либо: ошибки короткого замыкания (использует: обработка ошибок / исключений)
  • [] (монада списка): недетерминизм (использует: генерация списка, фильтрация, ...)
  • Состояние: одна изменяемая ссылка (использует: состояние)
  • Считыватель: общая среда (использует: привязки переменных, общая информация, ...)
  • Writer: выход или накопление "побочного канала" (использует: ведение журнала, ведение счетчика только для записи, ...)
  • Cont: нелокальный поток управления (использует: слишком много, чтобы перечислять)

Обычно вы должны обычно проектировать свою монаду, наложив на монадные трансформаторы стандартную Monad Transformer Library , который позволяет объединить вышеуказанные эффекты в одну монаду.Вместе они обрабатывают большинство монад, которые вы, возможно, захотите использовать.Есть несколько дополнительных монад, не включенных в MTL, таких как вероятность и поставка монад.

Что касается разработки интуиции для определения нового типаявляется монадой, и как она ведет себя как единое целое, вы можете думать о ней, поднимаясь с Functor до Monad:

  • Функтор позволяет преобразовывать значения с чистымfunctions.
  • Applicative позволяет встраивать чистые значения и экспресс-приложение - (<*>) позволяет переходить от встроенной функции и встроенного аргумента к встроенному результату.
  • Monad позволяет структуре встроенных вычислений зависеть от значений предыдущих вычислений.

Самый простой способ понять это - посмотреть на тип join:

join :: (Monad m) => m (m a) -> m a

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

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

Обратите внимание, что (>>=) может быть определено как

m >>= f = join (fmap f m)

, и поэтому монаду можно определить просто с помощью return и join (при условии, что это Functor; все монады являются аппликативными функторами, но иерархия классов типов Haskell, к сожалению, неЭто не требуется по историческим причинам ).

В качестве дополнительного примечания, вам, вероятно, не следует слишком сильно сосредотачиваться на монадах, независимо от того, какой шум они получают от заблуждающихся не Хаскеллеров,Существует много классов типов, которые представляют значимые и мощные шаблоны, и не все лучше всего выражать в виде монады. Аппликативный , Моноид , Складной ... какая абстракция для использования полностью зависит от вашей ситуации.И, конечно, то, что что-то является монадой, не означает, что не может быть и других вещей;Быть монадой - это просто еще одно свойство типа.

Итак, вам не следует слишком много думать о «идентификации монад»;вопросы больше похожи на:

  • Может ли этот код быть выражен в более простой монадической форме?С какой монадой?
  • Это тип, который я только что определил?Какими общими шаблонами, закодированными стандартными функциями на монадах, я могу воспользоваться?
15 голосов
/ 08 января 2012

Следуйте типам.

Если вы обнаружите, что написали функции для всех этих типов

  • (a -> b) -> YourType a -> YourType b
  • a -> YourType a
  • YourType (YourType a) -> YourType a

или все эти типы

  • a -> YourType a
  • YourType a -> (a -> YourType b) -> YourType b

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

(Помните, что вы можете переупорядочивать аргументы, например, YourType a -> (a -> b) -> YourType b - это просто (a -> b) -> YourType a -> YourType b в маскировке.)

Не обращайте внимания только на монады!Если у вас есть функции всех этих типов

  • YourType
  • YourType -> YourType -> YourType

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

7 голосов
/ 08 января 2012

Есть эффект просмотра монад:

  • Возможно - частичное / неисправное короткое замыкание
  • Либо - сообщение об ошибке / короткое замыкание (например, может быть с дополнительной информацией)
  • Writer - запись только "состояния", обычно ведение журнала
  • Reader - состояние только для чтения, обычно передача среды
  • Состояние - состояние чтения / записи
  • Возобновление - паузное вычисление
  • Список - несколько успехов

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

Важно отметить, что монад не так много. Есть некоторые экзотические, которых нет в стандартных библиотеках, например, монада вероятностей и вариации монады Cont, такие как Codensity. Но если вы не делаете что-то математическое, маловероятно, что вы придумаете (или обнаружите) новую монаду, однако, если вы будете использовать Haskell достаточно долго, вы построите много монад, которые представляют собой различные комбинации стандартных.

Edit - Также обратите внимание, что порядок, в котором вы складываете монадные трансформаторы, приводит к различным монадам:

Если вы добавите ErrorT (преобразователь) в монаду Writer, вы получите эту монаду Either err (log,a) - вы можете получить доступ к журналу, только если у вас нет ошибок.

Если вы добавите WriterT (transfomer) в монаду Error, вы получите эту монаду (log, Either err a), которая всегда дает доступ к журналу.

4 голосов
/ 09 января 2012

Это своего рода не ответ, но я чувствую, что важно сказать в любом случае. Просто спросите! StackOverflow, / r / haskell и канал #haskell irc - все это отличные места для быстрой обратной связи от умных людей. Если вы работаете над проблемой и подозреваете, что есть какая-то монадическая магия, которая может облегчить ее, просто спросите! Сообщество Haskell любит решать проблемы и смехотворно дружелюбно.

Не поймите меня неправильно, я не призываю вас никогда не учиться для себя. Напротив, взаимодействие с сообществом Haskell является одним из лучших способов обучения. LYAH и RWH , 2 книги Haskell, которые свободно доступны в Интернете, также очень рекомендуются.

О, и не забывайте играть, играть, играть! Когда вы будете играть с монадическим кодом, вы начнете чувствовать, что есть у монад в форме, и когда монадический комбинаторы могут быть полезны. Если вы катите свою собственную монаду, то обычно система типов приведет вас к очевидному, простому решению. Но, если честно, вам редко нужно запускать собственный экземпляр Monad, поскольку библиотеки Haskell предоставляют множество полезных вещей, как упоминали другие авторы.

0 голосов
/ 04 августа 2018

Существует общепринятое мнение, что во многих языках программирования можно увидеть «тег инфекционной функции» - какое-то специальное поведение для функции, которая должна распространяться и на ее вызывающих.

  • Функции Rust могутбыть unsafe, означая, что они выполняют операции, которые могут потенциально нарушить безопасность памяти.unsafe функции могут вызывать нормальные функции, но любая функция, которая вызывает функцию unsafe, должна быть также unsafe.
  • Функции Python могут быть async, то есть они возвращают обещание, а не фактическоезначение.async функции могут вызывать нормальные функции, но вызов функции async (через await) может выполняться только другой функцией async.
  • Функции Haskell могут быть нечистыми означает, что они возвращают IO a, а не a.Нечистые функции могут вызывать чистые функции, но нечистые функции могут вызываться только другими нечистыми функциями.
  • Математические функции могут быть частичными , то есть они не отображают каждое значение в своей области навыход.Определения частичных функций могут ссылаться на итоговые функции, но если тотальная функция отображает часть своей области в частичную функцию, она также становится частичной.

Хотя могут быть способы вызова теговой функциииз функции без тегов не существует общего способа, и это часто может быть опасным и угрожать нарушить абстракцию, которую пытается обеспечить язык.

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

Скажем, вы разработчик языка и узнаете этот шаблон, иВы решаете, что хотите разрешить пользовательские теги.Допустим, пользователь определил тег Err, представляющий вычисления, которые могут вызвать ошибку.Функция, использующая Err, может выглядеть следующим образом:

function div <Err> (n: Int, d: Int): Int
    if d == 0
        throwError("division by 0")
    else
        return (n / d)

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

function div(n: Int, d: Int): <Err> () -> Int
    () =>
        if d == 0
            throwError("division by 0")
        else
            return (n / d)

В ленивом языке, таком как Haskell, нам не нужно закрытиеи может просто возвращать ленивое значение напрямую:

div :: Int -> Int -> Err Int
div _ 0 = throwError "division by 0"
div n d = return $ n / d

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

class Tag m where

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

    addTag :: a -> m a

Мы также хотим иметь возможность принять значение с тегом (m a) и применить функцию с тегом (a -> m b), чтобы получить результат с тегом (m b):

    embed :: m a -> (a -> m b) -> m b

Это, конечно, именно определение монады!addTag соответствует return, а embed соответствует (>>=).

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

PS Что касается тегов, которые я упоминал в этом ответе: модели Haskellпримесь с монадой IO и частичное с монадой Maybe.Большинство языков реализуют асинхронные / обещания довольно прозрачно, и, похоже, существует пакет на Haskell под названием обещание , который имитирует эту функциональность.Монада Err эквивалентна монаде Either String.Я не знаю ни одного языка, который моделирует небезопасную память монадически, это могло бы быть сделано.

...