Какое преимущество дает Монад нам по Прикладной?

Я прочитал эту статью, но не понял последний раздел.

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

let maybeAge = (\futureYear birthYear -> if futureYear < birthYear
    then yearDiff birthYear futureYear
    else yearDiff futureYear birthYear) <$> (readMay futureYearString) <*> (readMay birthYearString)

Это ужасно, без do-syntax, но кроме того, я не понимаю, зачем нам нужна Monad. Может ли кто-нибудь прояснить это для меня?

Ответ 1

Вот несколько функций, которые используют интерфейс 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)

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

import Control.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

Выглядит хорошо! Он имеет правильный тип, это должно быть одно и то же! Позвольте просто проверить, чтобы убедиться.

*Main> ifM (Just True) (Just 1) (Just 2)
Just 1
*Main> ifM (Just True) (Just 1) (Nothing)
Just 1
*Main> ifA (Just True) (Just 1) (Just 2)
Just 1
*Main> ifA (Just True) (Just 1) (Nothing)
Nothing

И вот ваш первый намек на разницу. Вы не можете написать функцию, используя только интерфейс Applicative, который реплицирует ifM.

Если вы разделите это на мысли о значениях формы f a как о "эффектах" и "результатах" (оба из которых являются очень нечеткими приблизительными терминами, которые являются лучшими доступными условиями, но не очень хорошими) вы можете улучшить свое понимание здесь. В случае значений типа Maybe a "эффект" является успешным или неудачным, как вычисление. "Результат" - это значение типа a, которое может присутствовать при завершении вычислений. (Значения этих терминов в значительной степени зависят от конкретного типа, поэтому не думайте, что это допустимое описание чего-либо другого, кроме Maybe как типа.)

Учитывая эту настройку, мы можем немного разглядеть разницу. Интерфейс Applicative позволяет динамическому потоку "результат" быть динамическим, но для него требуется, чтобы поток управления "эффект" был статичным. Если ваше выражение включает в себя 3 вычисления, которые могут потерпеть неудачу, отказ любого из них вызывает сбой всего вычисления. Интерфейс Monad более гибкий. Он позволяет контролировать поток эффекта "эффект" по значениям "результат". ifM выбирает, какой аргумент "эффекты" включать в свои собственные "эффекты" на основе его первого аргумента. Это огромное фундаментальное различие между ifA и ifM.

Там что-то еще более серьезное происходит с whileM. Попробуйте сделать whileA и посмотрим, что произойдет.

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, а step x имеет тип f a. (<*>) - это не правильная форма, чтобы соответствовать им. Для его работы тип функции должен быть f (a -> a).

Вы можете попробовать еще много вещей, но в конечном итоге вы обнаружите, что whileA не имеет реализации, которая работает даже близко к способу whileM. Я имею в виду, вы можете реализовать этот тип, но просто нет возможности сделать его как замкнутым, так и замкнутым.

Для его работы требуется либо join, либо (>>=). (Ну, или один из многих эквивалентов одного из них) И те дополнительные вещи, которые вы получаете из интерфейса Monad.

Ответ 2

С монадами последующие эффекты могут зависеть от предыдущих значений. Например, вы можете:

main = do
    b <- readLn :: IO Bool
    if b
      then fireMissiles
      else return ()

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

В некоторой степени связаны:

Ответ 3

Как сказал Стивен Тетли в комментарии, этот пример фактически не использует контекст-чувствительность. Один из способов подумать о чувствительности к контексту заключается в том, что он позволяет использовать выбирать, какие действия следует принимать в зависимости от монадических значений. Апликативные вычисления всегда должны иметь одинаковую "форму" в определенном смысле независимо от значений; монадических вычислений не требуется. Я лично считаю, что это проще понять с конкретным примером, поэтому давайте посмотрим на него. Здесь две версии простой программы, которые просят ввести пароль, проверьте, что вы ввели правильный, и распечатайте ответ в зависимости от того, сделали ли вы.

import Control.Applicative

checkPasswordM :: IO ()
checkPasswordM = do putStrLn "What the password?"
                    pass <- getLine
                    if pass == "swordfish"
                      then putStrLn "Correct.  The secret answer is 42."
                      else putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"

checkPasswordA :: IO ()
checkPasswordA =   if' . (== "swordfish")
               <$> (putStrLn "What the password?" *> getLine)
               <*> putStrLn "Correct.  The secret answer is 42."
               <*> putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"

if' :: Bool -> a -> a -> a
if' True  t _ = t
if' False _ f = f

Загрузите это в GHCi и проверьте, что происходит с монадической версией:

*Main> checkPasswordM
What the password?
swordfish
Correct.  The secret answer is 42.
*Main> checkPasswordM
What the password?
zvbxrpl
INTRUDER ALERT!  INTRUDER ALERT!

До сих пор так хорошо. Но если мы используем аппликативную версию:

*Main> checkPasswordA
What the password?
hunter2
Correct.  The secret answer is 42.
INTRUDER ALERT!  INTRUDER ALERT!

Мы ввели неверный пароль, но у нас все еще есть секрет! И предупреждение злоумышленника! Это связано с тем, что <$> и <*>, или эквивалентно liftAn/liftMn, всегда выполняют эффекты всех своих аргументов. Применимая версия преобразует в обозначении do значение

do pass  <- putStrLn "What the password?" *> getLine)
   unit1 <- putStrLn "Correct.  The secret answer is 42."
   unit2 <- putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"
   pure $ if' (pass == "swordfish") unit1 unit2

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

do val1 <- app1
   val2 <- app2
   ...
   valN <- appN
   pure $ f val1 val2 ... valN

(где некоторым из appI разрешено иметь вид pure xI). И, что эквивалентно, любой монадический код в этой форме может быть переписан как

f <$> app1 <*> app2 <*> ... <*> appN

или эквивалентно как

liftAN f app1 app2 ... appN

Чтобы подумать об этом, рассмотрите методы Applicative:

pure  :: a -> f a
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b

И затем рассмотрим, что добавляет Monad:

(=<<) :: (a -> m b) -> m a -> m b
join  :: m (m a) -> m a

(Помните, что вам нужен только один из них.)

Многое, если вы думаете об этом, единственный способ собрать аппликативные функции - построить цепочки вида f <$> app1 <*> ... <*> appN и, возможно, вложить эти цепочки (например, f <$> (g <$> x <*> y) <*> z). Однако (=<<) (или (>>=)) позволяет нам принимать значение и производить разные монадические вычисления в зависимости от этого значения, которые могут быть построены "на лету". Это то, что мы используем, чтобы решить, следует ли вычислять "распечатать секрет" или вычислить "распечатать предупреждение о вторжении" и почему мы не можем принять это решение только с помощью аппликативных функторов; ни один из типов для аппликативных функций не позволяет использовать обычное значение.

Вы можете думать о join совместно с fmap аналогичным образом: как я упомянул в комментарии, вы можете сделать что-то вроде

checkPasswordFn :: String -> IO ()
checkPasswordFn pass = if pass == "swordfish"
                         then putStrLn "Correct.  The secret answer is 42."
                         else putStrLn "INTRUDER ALERT!  INTRUDER ALERT!"

checkPasswordA' :: IO (IO ())
checkPasswordA' = checkPasswordFn <$> (putStrLn "What the password?" *> getLine)

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

checkPasswordM' :: IO ()
checkPasswordM' = join checkPasswordA'

И это делает то же самое, что и предыдущая монадическая версия (пока мы import Control.Monad, чтобы получить join):

*Main> checkPasswordM'
What the password?
12345
INTRUDER ALERT!  INTRUDER ALERT!

Ответ 4

С другой стороны, здесь практический пример деления Applicative/Monad, где Applicative имеет преимущество: обработка ошибок! У нас явно есть реализация Monad Either, которая несет ошибки, но всегда заканчивается раньше.

Left e1 >> Left e2    ===   Left e1

Вы можете думать об этом как о влиянии смешения ценностей и контекстов. Поскольку (>>=) будет пытаться передать результат значения Either e a такой функции, как a -> Either e b, он должен немедленно выйти из строя, если вход Either равен Left.

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

data AllErrors e a = Error e | Pure a deriving (Functor)

instance Monoid e => Applicative (AllErrors e) where
  pure = Pure
  (Pure f) <*> (Pure x) = Pure (f x)
  (Error e) <*> (Pure _) = Error e
  (Pure _) <*> (Error e) = Error e
  -- This is the non-Monadic case
  (Error e1) <*> (Error e2) = Error (e1 <> e2)

Невозможно записать экземпляр Monad для AllErrors, так что ap соответствует (<*>), поскольку (<*>) использует возможность запуска как первого, так и второго контекстов перед использованием любых значений, чтобы получить как ошибки, так и (<>) их вместе. Monad ic (>>=) и (join) могут обращаться только к контекстам, переплетающимся со своими значениями. Вот почему экземпляр Either Applicative левый, поэтому он также может иметь гармоничный экземпляр Monad.

> Left "a" <*> Left "b"
Left 'a'

> Error "a" <*> Error "b"
Error "ab"

Ответ 5

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

Например, с помощью аппликативного анализатора последовательность действий синтаксического анализа фиксируется на все время. Это означает, что вы можете потенциально выполнить "оптимизацию" на нем. С другой стороны, я могу написать синтаксический анализатор Monadic, который анализирует некоторое описание грамматики BNF, динамически создает парсер для этой грамматики, а затем запускает этот анализатор над остальной частью ввода. Каждый раз, когда вы запускаете этот синтаксический анализатор, он потенциально создает новый синтаксический анализатор для анализа второй части ввода. Аппликатор не имеет надежды на такое, и нет возможности выполнять оптимизацию времени компиляции в парсере, который еще не существует...

Как вы можете видеть, иногда "ограничение" Аппликатора действительно полезно - и иногда требуется дополнительная мощность, предлагаемая Монадой, чтобы выполнить эту работу. Вот почему у нас есть оба.

Ответ 6

Если вы попытаетесь преобразовать подпись типа Monad bind и Applicative <*> в естественный язык, вы обнаружите, что:

bind: I предоставит вам содержащееся значение и вы вернете мне новое упакованное значение

<*>: Вы предоставили мне упакованную функцию, которая принимает содержащееся значение и возвращает значение, а I будет использовать его для создания нового упакованного значения на основе моих правил.

Теперь, как вы можете видеть из приведенного выше описания, bind дает вам больше контроля по сравнению с <*>

Ответ 7

Если вы работаете с Applicatives, "форма" результата уже определяется "формой" ввода, например. если вы вызываете [f,g,h] <*> [a,b,c,d,e], ваш результат будет состоять из 15 элементов, независимо от того, какие значения имеют переменные. У вас нет этой гарантии/ограничения с монадами. Рассмотрим [x,y,z] >>= join replicate: для [0,0,0] вы получите результат [], для [1,2,3] результат [1,2,2,3,3,3].

Ответ 8

Теперь, когда расширение ApplicativeDo стало довольно распространенным явлением, разницу между Monad и Applicative можно проиллюстрировать с помощью простого фрагмента кода.

С Monad вы можете сделать

do
   r1 <- act1
   if r1
        then act2
        else act3

но имеющий только Applicative делать-блок, вы не можете использовать, if на вещах, которые вы вытащили с <-.