直觉
一个粗略的直觉是 Monad 是一种特殊的容器 ( Functor
),你有两个可用的操作。return
将单个元素放入容器的包装操作。join
将容器的容器合并为单个容器的操作。
return :: Monad m => a -> m a
join :: Monad m => m (m a) -> m a
所以对于 Monad 也许你有:
return :: a -> Maybe a
return x = Just x
join :: Maybe (Maybe a) -> Maybe a
join (Just (Just x) = Just x
join (Just Nothing) = Nothing
join Nothing = Nothing
同样对于 Monad [ ] 这些操作被定义为:
return :: a -> [a]
return x = [x]
join :: [[a]] -> [a]
join xs = concat xs
Monad 的标准数学定义是基于这些返回和连接运算符。然而在 Haskell 中 Monad 类的定义用一个绑定操作符代替了连接。
Haskell 中的单子
在函数式编程语言中,这些特殊容器通常用于表示有效的计算。类型Maybe a
将表示可能成功或可能不成功的计算,而类型表示不确定[a]
的计算。特别是我们对具有效果的函数感兴趣,例如a->m b
一些具有类型的函数Monad m
。我们需要能够组合它们。这可以使用一元组合或绑定运算符来完成。
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
(>>=) :: Monad m => m a -> (a -> m b) -> m b
在 Haskell 中,后者是标准的。请注意,它的类型与应用程序运算符的类型非常相似(但带有翻转的参数):
(>>=) :: Monad m => m a -> (a -> m b) -> m b
flip ($) :: a -> (a -> b) -> b
它接受一个有效的函数f :: a -> m b
和一个返回类型的计算mx :: m a
值a
,并执行应用程序mx >>= f
。那么我们如何用 Monads 做到这一点呢?容器 ( Functors
) 可以被映射,在这种情况下,结果是计算中的计算,然后可以展平:
fmap f mx :: m (m b)
join (fmap f mx) :: m b
所以我们有:
(mx >>= f) = join (fmap f mx) :: m b
要在实践中看到这一点,请考虑一个带有列表(非确定性函数)的简单示例。假设您有一个可能的结果列表mx = [1,2,3]
和一个非确定性函数f x = [x-1, x*2]
。要计算mx >>= f
,首先将 mx 与 f 映射,然后合并结果::
fmap f mx = [[0,2],[1,4],[2,6]]
join [[0,2],[1,4],[2,6]] = [0,2,1,4,2,6]
由于在 Haskell 中绑定运算符(>>=)
比 更重要join
,因此出于效率原因,后者是从前者定义的,而不是相反。
join mx = mx >>= id
此外,使用 join 和 fmap 定义的绑定运算符也可用于定义映射操作。由于这个原因,Monad 不需要是类 Functor 的实例。liftM
在 Monad 库中调用 fmap 的等效操作。
liftM f mx = mx >>= \x-> return (f x)
所以 Monads Maybe 的实际定义变成:
return :: a -> Maybe a
return x = Just x
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Nothing >>= f = Nothing
Just x >>= f = f x
对于 Monad [ ]:
return :: a -> [a]
return x = [x]
(>>=) :: [a] -> (a -> [b]) -> [b]
xs >>= f = concat (map f xs)
= concatMap f xs -- same as above but more efficient
在设计自己的 Monad 时,您可能会发现更容易,而不是尝试直接定义(>>=)
,将问题分成几部分并弄清楚如何映射和连接您的结构。拥有 map 和 join 也可以用于验证您的 Monad 是否定义良好,即它是否满足所需的法律。
单子定律
你的 Monad 应该是一个 Functor,所以映射操作应该满足:
fmap id = id
fmap g . fmap f = fmap (g . f)
返回和加入的规律是:
join . return = id
join . fmap return = id
join . join = join . fmap join
前两个定律指定合并撤消换行。如果您将一个容器包装在另一个容器中,则 join 会返回原始容器。如果您使用包装操作映射容器的内容,则再次加入会返回您最初拥有的内容。最后一条定律是连接的结合性。如果您有三层容器,则通过从内部或外部合并可以获得相同的结果。
同样,您可以使用 bind 而不是 join 和 fmap。你得到更少但(可以说)更复杂的法律:
return a >>= f = f a
m >>= return = m
(m >>= f) >>= g = m >>= (\x -> f x >>= g)