Зачем использовать такой особый тип функции в монадах?
Новое в Haskell, и я пытаюсь понять эту вещь Монады. Оператор монадического связывания - >>=
- имеет очень своеобразную подпись типа:
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Чтобы упростить, заменим Maybe
на m
:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Однако обратите внимание, что определение могло быть записано тремя различными способами:
(>>=) :: Maybe a -> (Maybe a -> Maybe b) -> Maybe b
(>>=) :: Maybe a -> ( a -> Maybe b) -> Maybe b
(>>=) :: Maybe a -> ( a -> b) -> Maybe b
Из трех один в центре является наиболее асимметричным. Тем не менее, я понимаю, что первый из них не имеет смысла, если мы хотим избежать (то, что LYAH вызывает шаблонный код). Однако из следующих двух я предпочел бы последний. Для Maybe
это будет выглядеть так:
Если это определено как:
(>>=) :: Maybe a -> (a -> b) -> Maybe b
instance Monad Maybe where
Nothing >>= f = Nothing
(Just x) >>= f = return $ f x
Здесь a -> b
- обычная функция. Кроме того, я не вижу ничего опасного, потому что Nothing
ловит исключение перед приложением функции, поэтому функция a -> b
не будет вызываться, если не получена Just a
.
Так что, возможно, есть что-то, что не кажется мне очевидным, что привело к тому, что определение (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
было предпочтительнее более простого определения (>>=) :: Maybe a -> (a -> b) -> Maybe b
? Есть ли какая-то неотъемлемая проблема, связанная с (что я считаю) более простым определением?
Ответы
Ответ 1
Это гораздо более симметрично, если вы думаете в терминах следующей производной функции (от Control.Monad
):
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
(f >=> g) x = f x >>= g
Причина, по которой эта функция значительна, заключается в том, что она подчиняется трем полезным уравнениям:
-- Associativity
(f >=> g) >=> h = f >=> (g >=> h)
-- Left identity
return >=> f = f
-- Right identity
f >=> return = f
Это законы категории, и если вы переводите их на использование (>>=)
вместо (>=>)
, вы получите три закона монады:
(m >>= g) >>= h = m >>= \x -> (g x >>= h)
return x >>= f = f x
m >>= return = m
Так что это действительно не (>>=)
, это элегантный оператор, а (>=>)
- симметричный оператор, который вы ищете. Однако причина, по которой мы обычно думаем в терминах (>>=)
, состоит в том, что это то, что обозначение do
desugars to.
Ответ 2
Рассмотрим одно из общих применений ошибок Maybe
monad: обработки. Скажем, я хотел благополучно разделить два номера. Я мог бы написать эту функцию:
safeDiv :: Int -> Int -> Maybe Int
safeDiv _ 0 = Nothing
safeDiv n d = n `div` d
Затем со стандартной монадой Maybe
я мог бы сделать что-то вроде этого:
foo :: Int -> Int -> Maybe Int
foo a b = do
c <- safeDiv 1000 b
d <- safeDiv a c -- These last two lines could be combined.
return d -- I am not doing so for clarity.
Обратите внимание, что на каждом шаге safeDiv
может выйти из строя, но на обоих шагах safeDiv
принимает Int
s, а не Maybe Int
s. Если >>=
имела эту подпись:
(>>=) :: Maybe a -> (a -> b) -> Maybe b
Вы могли бы скомпоновать функции вместе, а затем дать ему либо Nothing
, либо Just
, либо либо развернуть Just
, пройти весь конвейер, но и перевернуть его в Just
, или он просто передал бы Nothing
, по существу, нетронутым. Это может быть полезно, но это не монада. Чтобы это было полезно, мы должны быть в состоянии провалиться посередине и что эта подпись дает нам:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Кстати, что-то с созданной вами подписи существует:
flip fmap :: Maybe a -> (a -> b) -> Maybe b
Ответ 3
Более сложная функция с a -> Maybe b
является более общей и более полезной и может быть использована для реализации простой. Это не работает наоборот.
Вы можете построить функцию a -> Maybe b
из функции f :: a -> b
:
f' :: a -> Maybe b
f' x = Just (f x)
Или, в терминах return
(который Just
для Maybe
):
f' = return . f
Другой способ не обязательно возможен. Если у вас есть функция g :: a -> Maybe b
и вы хотите использовать ее с "простым" связыванием, вам сначала нужно преобразовать ее в функцию a -> b
. Но это обычно не работает, потому что g
может возвращать Nothing
, где функция a -> b
должна возвращать значение b
.
Таким образом, как правило, "простая" привязка может быть реализована с точки зрения "сложной", но не наоборот. Кроме того, сложная привязка часто полезна и не может сделать многое невозможным. Поэтому, используя более общие мозаики связывания, применимы к большему количеству ситуаций.
Ответ 4
Проблема с альтернативной сигнатурой типа для (>>=)
заключается в том, что она только случайно работает для монады Maybe, если вы попробуете ее с другой монадой (то есть List monad), вы увидите, что она разбивается на тип b
для общего случая. Подпись, которую вы предоставили, не описывает монадическое связывание, и законы монады не могут не соответствовать этому определению.
import Prelude hiding (Monad, return)
-- assume monad was defined like this
class Monad m where
(>>=) :: m a -> (a -> b) -> m b
return :: a -> m a
instance Monad Maybe where
Nothing >>= f = Nothing
(Just x) >>= f = return $ f x
instance Monad [] where
m >>= f = concat (map f m)
return x = [x]
Ошибка с ошибкой типа:
Couldn't match type `b' with `[b]'
`b' is a rigid type variable bound by
the type signature for >>= :: [a] -> (a -> b) -> [b]
at monadfail.hs:12:3
Expected type: a -> [b]
Actual type: a -> b
In the first argument of `map', namely `f'
In the first argument of `concat', namely `(map f m)'
In the expression: concat (map f m)
Ответ 5
Вещь, которая делает монаду монадой, - это то, как работает "join" . Напомним, что соединение имеет тип:
join :: m (m a) -> m a
"join" is is "интерпретирует" действие монады, которое возвращает действие монады в терминах действия монады. Таким образом, вы можете подумать, что он отрывает слой монады (или, лучше всего, вытаскивает материал во внутренний слой во внешний слой). Это означает, что "m" формирует "стек", в смысле "стека вызовов". Каждый "m" представляет собой контекст, и "join" позволяет нам объединять контексты вместе в порядке.
Итак, что это связано с привязкой? Напомним:
(>>=) :: m a -> (a -> m b) -> m b
А теперь рассмотрим, что для f:: a → m b и ma:: m a:
fmap f ma :: m (m b)
То есть результат применения f непосредственно к a в ma является (m (m b)). Мы можем применить к этому присоединение, чтобы получить m b. Короче говоря,
ma >>= f = join (fmap f ma)