Разница между Монадой и Аппликатором в Хаскелле
Я просто прочитал следующее из 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 = кое-что".