Разница между Монадой и Аппликативным в Хаскеле
Я только что прочитал следующее из typeclassopedia о разнице между Monad
а также Applicative
, Я могу понять, что нет join
в Applicative
, Но следующее описание кажется мне расплывчатым, и я не мог понять, что именно подразумевается под "результатом" монадического вычисления / действия. Итак, если я поставлю значение в Maybe
, что делает монаду, что является результатом этого "вычисления"?
Давайте внимательнее посмотрим на тип (>>=). Основная интуиция состоит в том, что она объединяет два вычисления в одно большее вычисление. Первый аргумент, ma, является первым вычислением. Однако было бы скучно, если бы вторым аргументом был просто m b; тогда у вычислений не будет возможности взаимодействовать друг с другом (на самом деле, именно так и происходит с Applicative). Таким образом, второй аргумент (>> =) имеет тип a -> m b: функция этого типа, учитывая результат первого вычисления, может произвести второе вычисление для запуска.... Интуитивно понятно, что именно эта способность использовать выходные данные предыдущих вычислений, чтобы решить, какие вычисления следует выполнить дальше, делает Monad более мощным, чем Applicative. Структура аппликативного вычисления является фиксированной, тогда как структура вычисления монады может изменяться в зависимости от промежуточных результатов.
Есть ли конкретный пример, иллюстрирующий "способность использовать выходные данные предыдущих вычислений, чтобы решить, какие вычисления запустить дальше", которых нет в Applicative?
7 ответов
Мой любимый пример - "чисто аппликативный Либо". Начнем с анализа базового экземпляра Monad для Either
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
Теперь реализация (<*>)
это особая часть, которую стоит тщательно рассмотреть Он выполняет некоторое количество "короткого замыкания" в своих первых 3 случаях, но делает что-то интересное в четвертом.
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
если есть 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
точно воплощает идею вычисления результата в зависимости от значений, заложенных в вычислениях аргументов.
Just 1
описывает "вычисление", чей "результат" равен 1. Nothing
описывает вычисления, которые не дают результатов.
Разница между Монадой и Аппликативом в том, что в Монаде есть выбор. Ключевым отличием монад является возможность выбирать между различными путями в вычислениях (а не просто прорваться пораньше). В зависимости от значения, полученного на предыдущем этапе вычислений, остальная часть вычислительной структуры может измениться.
Вот что это значит. В монадической цепочке
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 )
все функции работают "внутри" вычислений, нет никакой возможности разорвать цепь. Каждая функция просто преобразует значение, которое она получает. "Форма" вычислительной структуры полностью "снаружи" с точки зрения функций.
Функция может возвращать специальное значение, указывающее на ошибку, но не может пропустить следующие шаги в вычислении. Все они должны будут обрабатывать специальное значение также особым образом. Форма расчета не может быть изменена в соответствии с полученным значением.
С помощью монад функции сами строят вычисления по своему выбору.
Ключ разницы можно наблюдать в типе ap
против типа =<<
,
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)
может "решить", будет ли применена связанная функция затем - путем создания 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
Вот мой взгляд на @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 b (\x -> if x then t else e)
Проблема с типом возврата ifA'
, который f (f a)
, В Applicative
нет способа свалить два вложенных Applicative
S в одном. Но эта разрушающаяся функция - именно то, что join
в Monad
выполняет. Так,
ifA = join . ifA'
будет удовлетворять уравнениям для ifA
, если мы можем реализовать join
соответственно. Какие Applicative
отсутствует здесь именно join
функция. Другими словами, мы можем как-то использовать результат из предыдущего результата в Applicative
, Но делать это в Applicative
Framework будет включать в себя увеличение типа возвращаемого значения до вложенного аппликативного значения, которое у нас нет средств для возврата к одноуровневому аппликативному значению. Это будет серьезной проблемой, потому что, например, мы не можем составлять функции, используя Applicative
S соответственно. С помощью join
решает проблему, но само введение join
продвигает Applicative
к Monad
,
Но следующее описание кажется мне расплывчатым, и я не мог понять, что именно подразумевается под "результатом" монадического вычисления / действия.
Что ж, эта неопределенность несколько умышленная, потому что "результат" монадических вычислений зависит от каждого типа. Лучший ответ немного тавтологичен: "результат" (или результаты, так как их может быть больше одного) - это любое значение (значения) реализации экземпляра (>>=) :: 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
против ifM
разница, поэтому я подумал, что выделю еще одно существенное отличие: аппликативные сочинения, а монады - нет. С Monad
с, если вы хотите сделать 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 @f@ 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
Как объясняет @Will Ness в своем ответе, ключевое отличие состоит в том, что с Monads есть выбор между различными путями выполнения на каждом шаге. Давайте сделаем этот потенциальный выбор синтаксически видимым, реализовав функцию для четырехкратного упорядочивания. Один раз для аппликативного
f
, а затем для монады
m
:
seq4A :: Applicative f => f a -> f [a]
seq4A f =
f <**> (
f <**> (
f <**> (
f <&> (\a1 a2 a3 a4 ->
[a1, a2, a3, a4]))))
seq4M :: Monad m => m a -> m [a]
seq4M m =
m >>= (\a1 ->
m >>= (\a2 ->
m >>= (\a3 ->
m >>= (\a4 ->
return [a1, a2, a3, a4]))))
В
seq4M
функция имеет значения, полученные в результате монадического действия, доступные на каждом шаге и, таким образом, может делать выбор на каждом шаге. С другой стороны
seq4A
функция имеет значения, доступные только в самом конце.
Я хотел бы поделиться своим мнением об этой "странной вещи", так как я понимаю, что все внутри контекста применяется, например, так:
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 = кое-что".