Разница между Монадой и Аппликатором в Хаскелле

Я просто прочитал следующее из typeclassopedia о разнице между Monad и Applicative. Я могу понять, что в Applicative нет join. Но следующее описание выглядит неопределенным для меня, и я не мог понять, что именно подразумевается под "результатом" монадического вычисления/действия. Итак, если я помещаю значение в Maybe, что делает монаду, что является результатом этого "вычисления"?

Давайте посмотрим более внимательно на тип ( → =). Основная интуиция что он объединяет два вычисления в одно большее вычисление. первый аргумент, m a, является первым вычислением. Однако это было бы скучный, если второй аргумент был всего лишь m b; то не было бы способ для вычислений взаимодействовать друг с другом (на самом деле это это как раз ситуация с аппликативным). Итак, второй аргумент ( → =) имеет тип a → m b: функция этого типа, полученная в результате первое вычисление, может произвести второе вычисление для запуска.... Интуитивно, именно эта способность использовать вывод из предыдущего вычисления, чтобы решить, какие вычисления следует запускать дальше, что делает Monad более мощный, чем аппликативный. Структура аппликативного вычисление фиксировано, тогда как структура вычисления Монады может изменение на основе промежуточных результатов.

Есть ли конкретный пример, иллюстрирующий "способность использовать вывод из предыдущих вычислений, чтобы решить, какие вычисления будут выполняться дальше", который не имеет? Аппликатив не имеет?

Ответ 1

Моим любимым примером является "чисто аппликативный Либо". Мы начнем с анализа базового экземпляра Monad для Либо

instance Monad (Either e) where
  return = Right
  Left e  >>= _ = Left e
  Right a >>= f = f a

Этот экземпляр имеет очень естественное короткое замыкание: мы исходим слева направо, и как только одно вычисление "терпит неудачу" в Left, тогда все остальное тоже. Также существует естественный экземпляр Applicative, который имеет любой Monad

instance Applicative (Either e) where
  pure  = return
  (<*>) = ap

где ap - это не более чем последовательность слева направо до return:

ap :: Monad m => m (a -> b) -> m a -> m b
ap mf ma = do 
  f <- mf
  a <- ma
  return (f a)

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

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

Если мы думаем о m a как "прошлом" и m b как "будущем", то (>>=) создает будущее из прошлого, пока оно может запускать "шаговый" (a -> m b). Этот "степпер" требует, чтобы значение a действительно существовало в будущем... и это невозможно для Either. Поэтому (>>=) требует короткого замыкания.

Поэтому вместо этого мы реализуем экземпляр Applicative, который не может иметь соответствующий Monad.

instance Monoid e => Applicative (Either e) where
  pure = Right

Теперь реализация (<*>) - это особая часть, которую стоит тщательно рассмотреть. Он выполняет некоторое количество "короткого замыкания" в первых трех случаях, но делает что-то интересное в четвертом.

  Right f <*> Right a = Right (f a)     -- neutral
  Left  e <*> Right _ = Left e          -- short-circuit
  Right _ <*> Left  e = Left e          -- short-circuit
  Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!

Заметим еще раз, что если мы рассмотрим левый аргумент как "прошлое" и правый аргумент как "будущее", то (<*>) является особым по сравнению с (>>=), поскольку он позволил "открыть" будущее и прошлое параллельно, а не обязательно нуждаются в результатах от "прошлого", чтобы вычислить "будущее".

Это означает, что мы можем использовать наш чисто Applicative Either для сбора ошибок, игнорируя Right, если в цепочке21 > ​​существует Left

> Right (+1) <*> Left [1] <*> Left [2]
> Left [1,2]

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

ifA :: Applicative f => f Bool -> f a -> f a -> f a

которая удовлетворяет следующим уравнениям

ifA (pure True)  t e == t
ifA (pure False) t e == e

в то время как мы можем написать ifM

ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM mbool th el = do
  bool <- mbool
  if bool then th else el

такое, что

ifM (return True)  t e == t
ifM (return False) t e == e

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

Ответ 2

Just 1 описывает "вычисление", чей "результат" равен 1. Nothing описывает вычисление, которое не дает результатов.

Разница между Монадой и Аппликатором заключается в том, что в Монаде есть выбор. Ключевым отличием Monads является способность выбирать между разными путями при вычислении (а не просто рано выходить). В зависимости от значения, полученного на предыдущем этапе вычисления, остальная структура вычислений может измениться.

Вот что это значит. В монадической цепочке

return 42            >>= (\x ->
if x == 1
   then
        return (x+1) 
   else 
        return (x-1) >>= (\y -> 
        return (1/y)     ))

if выбирает, какое вычисление нужно построить.

В случае применения, в

pure (1/) <*> ( pure (+(-1)) <*> pure 1 )

все функции работают "внутри" вычислений, нет возможности разбить цепочку. Каждая функция просто преобразует значение, которое оно подает. "Форма" вычислительной структуры полностью "снаружи" с точки зрения функций.

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

С монадами, сами функции строят вычисления по своему выбору.

Ответ 3

Вот мой взгляд на @J. Пример Абрахамсона относительно того, почему ifA не может использовать значение внутри, например, (pure True). По сути, это все еще сводится к отсутствию функции join от Monad в Applicative, которая объединяет две различные точки зрения, приведенные в typeclassopedia, чтобы объяснить разницу между Monad и Applicative.

Так что используя @J. Абрахамсон пример чисто аппликативного Either:

instance Monoid e => Applicative (Either e) where
  pure = Right

  Right f <*> Right a = Right (f a)     -- neutral
  Left  e <*> Right _ = Left e          -- short-circuit
  Right _ <*> Left  e = Left e          -- short-circuit
  Left e1 <*> Left e2 = Left (e1 <> e2) -- combine!

(который имеет эффект короткого замыкания для Either Monad), и функция ifA

ifA :: Applicative f => f Bool -> f a -> f a -> f a

Что если мы попытаемся достичь упомянутых уравнений:

ifA (pure True)  t e == t
ifA (pure False) t e == e

?

Ну, как уже указывалось, в конечном счете, содержание (pure True) не может быть использовано для более поздних вычислений. Но с технической точки зрения это не правильно. Мы можем использовать содержимое (pure True) поскольку Monad также является Functor с fmap. Мы можем:

ifA' b t e = fmap (\x -> if x then t else e) b

Проблема заключается в типе возврата ifA', который является f (fa). В Applicative невозможно объединить два вложенных Applicative в один. Но эта свертывающая функция - именно то, что выполняет join в Monad. Так,

ifA = join . ifA' 

будет удовлетворять уравнениям для ifA, если мы сможем реализовать join соответствующим образом. В данном случае Applicative не хватает именно функции join. Другими словами, мы можем каким-то образом использовать результат из предыдущего результата в Applicative. Но выполнение в Applicative среде будет включать в себя увеличение типа возвращаемого значения до вложенного аппликативного значения, которое у нас нет средств для возврата к одноуровневому аппликативному значению. Это будет серьезной проблемой, потому что, например, мы не можем составлять функции, используя Applicative надлежащим образом. Использование join решает проблему, но само введение join продвигает Applicative к Monad.

Ответ 4

Ключ разницы можно наблюдать в типе ap vs type =<<.

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

В обоих случаях существует m a, но только во втором случае m a может решить, применяется ли функция (a->m b). В свою очередь, функция (a->m b) может "решить", будет ли применена функция bound next - путем создания такого m b, который не содержит "b (например, [], Nothing или Left)).

В Applicative нет возможности для функций "внутри" m (a->b) делать такие "решения" - они всегда выдает значение типа b.

f 1 = Nothing -- here f "decides" to produce Nothing
f x = Just x

Just 1 >>= f >>= g -- g doesn't get applied, because f decided so.

В Applicative это невозможно, поэтому нельзя показать пример. Самое близкое:

f 1 = 0
f x = x

g <$> f <$> Just 1 -- oh well, this will produce Just 0, but can't stop g
                   -- from getting applied

Ответ 5

Но следующее описание выглядит неопределенным для меня, и я не мог понять, что именно подразумевается под "результатом" монадического вычисления/действия.

Ну, эта неопределенность несколько преднамеренная, потому что то, что "результат" имеет монадическое вычисление, зависит от каждого типа. Лучший ответ - немного тавтологический: "результат" (или результат, поскольку может быть больше одного) - это любое значение (-ы), реализация экземпляра (>>=) :: Monad m => m a -> (a -> m b) -> m b вызывает аргумент функции с.

Итак, если я поместил значение в Maybe, что делает монаду, что является результатом этого "вычисления"?

Монада Maybe выглядит следующим образом:

instance Monad Maybe where
    return = Just
    Nothing >>= _ = Nothing
    Just a >>= k = k a

Единственное, что здесь квалифицируется как "результат" , это a во втором уравнении для >>=, потому что это единственное, что когда-либо получает "питание" ко второму аргументу >>=.

Другие ответы углубились в разницу ifA vs. ifM, поэтому я подумал, что выделил бы еще одну значительную разницу: составления аппликаций, монады не. Если Monad s, если вы хотите сделать Monad, который объединяет эффекты двух существующих, вы должны переписать один из них в качестве монадного трансформатора. Напротив, если у вас есть два Applicatives, вы можете легко сделать более сложный из них, как показано ниже. (Код копируется с transformers.)

-- | The composition of two functors.
newtype Compose f g a = Compose { getCompose :: f (g a) }

-- | The composition of two functors is also a functor.
instance (Functor f, Functor g) => Functor (Compose f g) where
    fmap f (Compose x) = Compose (fmap (fmap f) x)

-- | The composition of two applicatives is also an applicative.
instance (Applicative f, Applicative g) => Applicative (Compose f g) where
    pure x = Compose (pure (pure x))
    Compose f <*> Compose x = Compose ((<*>) <$> f <*> x)


-- | The product of two functors.
data Product f g a = Pair (f a) (g a)

-- | The product of two functors is also a functor.
instance (Functor f, Functor g) => Functor (Product f g) where
    fmap f (Pair x y) = Pair (fmap f x) (fmap f y)

-- | The product of two applicatives is also an applicative.
instance (Applicative f, Applicative g) => Applicative (Product f g) where
    pure x = Pair (pure x) (pure x)
    Pair f g <*> Pair x y = Pair (f <*> x) (g <*> y)


-- | The sum of a functor @[email protected] with the 'Identity' functor
data Lift f a = Pure a | Other (f a)

-- | The sum of two functors is always a functor.
instance (Functor f) => Functor (Lift f) where
    fmap f (Pure x) = Pure (f x)
    fmap f (Other y) = Other (fmap f y)

-- | The sum of any applicative with 'Identity' is also an applicative 
instance (Applicative f) => Applicative (Lift f) where
    pure = Pure
    Pure f <*> Pure x = Pure (f x)
    Pure f <*> Other y = Other (f <$> y)
    Other f <*> Pure x = Other (($ x) <$> f)
    Other f <*> Other y = Other (f <*> y)

Теперь, если добавить в функтор Constant функтор/аппликатив:

newtype Constant a b = Constant { getConstant :: a }

instance Functor (Constant a) where
    fmap f (Constant x) = Constant x

instance (Monoid a) => Applicative (Constant a) where
    pure _ = Constant mempty
    Constant x <*> Constant y = Constant (x `mappend` y)

... мы можем собрать "аппликативный Either" из других ответов из Lift и Constant:

type Error e a = Lift (Constant e) a

Ответ 6

Я хотел бы поделиться своим мнением об этой "странной вещи", так как я понимаю, что все внутри контекста применяется, например, так:

iffy :: Applicative f => f Bool -> f a -> f a -> f a
iffy fb ft fe = cond <$> fb <*> ft <*> fe   where
            cond b t e = if b then t else e

case 1>> iffy (Just True) (Just "True") Nothing ->> Nothing

Уппс должен быть просто "True"... но

 case 2>> iffy (Just False) (Just "True") (Just "False") ->> Just "False" 

("хороший" выбор делается внутри контекста). Я объяснил это себе таким образом, незадолго до конца вычисления, в случае, если >> 1 мы получаем что-то подобное в "цепочке":

Just (Cond True "True") <*> something [something being "accidentaly" Nothing]

который согласно определению Аппликатив оценивается как:

fmap (Cond True "True") something 

который, когда "что-то" равно Nothing, становится Nothing в соответствии с ограничением Functor (fmap over Nothing дает Nothing). И невозможно определить Функтор с концом истории "fmap f Nothing = кое-что".