Монады - где они нужны?

На днях я говорил о функциональном программировании - особенно Haskell с некоторыми парнями Java/ Scala, и они спросили меня, что такое Monads и где они необходимы.

Ну, определение и примеры не были такими трудными - Maybe Monad, IO Monad, State Monad и т.д., поэтому все были, по крайней мере частично, в порядке со мной, говоря, что Монады - хорошая вещь.

Но где нужны Монады - Maybe можно избежать с помощью магических значений типа -1 в настройках Integer или "" в настройках String. Я написал игру без State Monad, что совсем не приятно, но новички делают это.

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

ИЗМЕНИТЬ

Я думаю, что мне нужно пояснить, что я не думаю, что использование "Магических значений" - хорошее решение, но многие программисты используют их, особенно на языках низкого уровня, как C или в сценариях SHell, где ошибка часто подразумевается возвратом -1.

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

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

Статья @Antal SZ относится к большой вы могли бы придумать монады, я просмотрел ее и обязательно прочитаю, когда у меня есть больше времени. Более откровенный ответ скрыт в комментарии с сообщением в блоге, на которое ссылается @Antal SZ Я помню время до монадов, которое было материалом я искал, когда я задал вопрос.

Ответ 1

Я не думаю, что тебе нужны монады. Это всего лишь образец, который проявляется естественным образом, когда вы работаете с определенными функциями. Лучшее объяснение этой точки зрения, которую я когда-либо видел, - это Дэн Пипони (sigfpe), отличный пост в блоге "Вы могли бы придумать монады! (И, возможно, у вас уже есть.)" , на который этот ответ вдохновлен.

Ты говоришь, что написал игру, не используя государственную монаду. Как это выглядело? Там у вас была хорошая возможность работать с функциями с типами, которые выглядели примерно как openChest :: Player -> Location -> (Item,Player) (который открывает сундук, возможно, повреждает игрока с ловушкой и возвращает найденный элемент). Как только вам нужно их объединить, вы можете сделать это вручную (let (item,player') = openChest player loc ; (x,player'') = func2 player' y in ...) или переопределить оператор monad >>=.

Или предположим, что мы работаем на языке с хэш-картами/ассоциативными массивами, и мы не работаем с монадами. Нам нужно найти несколько предметов и работать с ними; возможно, мы пытаемся отправить сообщение между двумя пользователями.

send username1 username2 = {
    user1 = users[username1]
    user2 = users[username2]
    sendMessage user1 user2 messageBody
}

Но подождите, это не сработает; username1 и username2 могут отсутствовать, и в этом случае они будут nil или -1 или что-то вместо желаемого значения. Или, возможно, поиск ключа в ассоциативном массиве возвращает значение типа Maybe a, поэтому это даже будет ошибка типа. Вместо этого нам нужно написать что-то вроде

send username1 username2 = {
    user1 = users[username1]
    if (user1 == nil) return
    user2 = users[username2]
    if (user2 == nil) return
    sendMessage user1 user2 messageBody
}

Или, используя Maybe,

send username1 username2 =
    case users[username1] of
      Just user1 -> case users[username2] of
                      Just user2 -> Just $ sendMessage user1 user2 messageBody
                      Nothing    -> Nothing
      Nothing    -> Nothing

Ик! Это грязное и чрезмерно вложенное. Поэтому мы определяем какую-то функцию, которая объединяет возможные неудачные действия. Может быть, что-то вроде

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
f >>= Just x  = f x
f >>= Nothing = Nothing

Итак, вы можете написать

send username1 username2 =
  users[username1] >>= $ \user1 ->
  users[username2] >>= $ \user2 ->
  Just (sendMessage user1 user2 messageBody)

Если вы действительно не хотели использовать Maybe, вы могли бы реализовать

f >>= x = if x == nil then nil else f x

Тот же принцип применяется.

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

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

Ответ 2

Я мог бы принять фразу "где/ X необходимо и неизбежно?" где X - это что-то вообще в вычислениях; в чем смысл?

Вместо этого, я думаю, более ценно спросить: "какое значение делает/делает X??".

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

Хорошо, но вам не нужна абстракция, верно? Я имею в виду, я могу просто набрать небольшой код вручную, который делает то же самое, не так ли? Да, конечно, все это всего лишь куча 0 и 1, поэтому давайте посмотрим, кто может написать парсер XML быстрее, я использую Java/Haskell/C или вы с машиной Тьюринга.


Re monads: поскольку монады обычно имеют дело с эффективными вычислениями, эта абстракция наиболее полезна при создании эффективных функций.

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

Я также хотел бы отметить, что монады не являются фундаментальными для Haskell:

  • do -нотация - это просто синтаксический сахар и может быть полностью заменена на >>= и >> без потери выразительности

  • они (и их комбинаторы, такие как join, >>=, mapM и т.д.) могут быть записаны в Haskell

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

Ответ 3

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

Например, класс Option в Java 8 устанавливает правило, что содержащееся значение присутствует либо не равно null, либо отсутствует. Пока вы используете дополнительный класс, с использованием или без использования flatMap, вы обмениваете это правило вокруг содержимого данных. Никто не может обмануть или забыть и добавить value = null с present = true.

Таким образом, объявляя вне кода, что -1 будет контрольным значением и означает, что такое-то и так хорошо, но вы по-прежнему полагаетесь на себя и других людей, работающих в коде, чтобы почтить эту семантику. Если новый парень приходит на борт и начинает использовать -1000000 для обозначения того же, то семантика должна быть принудительно введена вне кода (возможно, с помощью ведущего канала?), А не через механизмы кода.

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

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

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

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