定义,法律和例证
可能重复:
什么是monad?
我正在学习使用Haskell的函数式语言进行编程,并且在学习解析器时遇到了Monads 。 我以前从来没有听说过他们,所以我做了一些额外的研究,以了解他们是什么。
我为了学习这个主题而到处看到的只是让我感到困惑。 我无法真正找到Monad是什么以及如何使用它们的简单定义。 “monad是一种使用这些值根据值和计算序列构造计算结构的方法” - eh ???
有人可以提供一个Monad在Haskell中的简单定义,与它们相关的法律并给出一个例子吗?
直觉
粗略的直觉是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的定义将绑定运算符用于加入。
Monas在Haskell
在函数式编程语言中,这些特殊的容器通常用于表示有效的计算。 Maybe a
类型Maybe a
代表可能成功或不成功的计算,而类型[a]
是非确定性的计算。 特别是我们对具有效果的函数感兴趣,例如某些Monad m
类型为a->mb
。 我们需要能够撰写它们。 这可以使用monadic组合或绑定运算符来完成。
(>=>) :: 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 -> mb
和一个计算mx :: ma
返回类型为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]
和一个非确定性函数fx = [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类的实例。 在Monad库中对fmap的等效操作称为liftM
。
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
在设计自己的Monads时,你可能会发现它更容易,而不是试图直接定义(>>=)
,将问题分成几部分,找出如何映射和加入结构。 通过映射和连接也可以用于验证Monad是否定义良好,即它符合所需的法律。
Monad Laws
你的Monad应该是一个Functor,所以映射操作应该满足:
fmap id = id
fmap g . fmap f = fmap (g . f)
返回和加入的法律是:
join . return = id
join . fmap return = id
join . join = join . fmap join
前两项法律规定合并撤销包装。 如果你在另一个容器中包装容器,加入会让你回到原来的位置。 如果您使用包装操作映射容器的内容,再次加入会让您回到最初的样子。 最后一条法则是加入的关联性。 如果您有三层容器,则通过从内部或外部合并获得相同的结果。
再次,您可以使用bind而不是join和fmap。 你会减少,但(可以说)更复杂的法律:
return a >>= f = f a
m >>= return = m
(m >>= f) >>= g = m >>= (x -> f x >>= g)
Haskell中的monad有两种操作定义:
(>>=) :: Monad m => m a -> (a -> m b) -> m b -- also called bind
return :: Monad m => a -> m a
如果你没有诀窍,那么这两项行动就需要满足某些法律,这些法律在这一点上可能会让你感到困惑。 从概念上讲,您使用绑定来操作单值级别的值,然后返回来创建来自“微不足道”值的单值值。 例如,
getLine :: IO String
,
所以你不能修改和putStrLn
这个String
- 因为它不是一个String
而是一个IO String
!
那么,我们有一个IO Monad方便,所以不用担心。 我们所要做的就是使用bind来做我们想要的。 让我们来看看IO Monad中的绑定:
(>>=) :: IO a -> (a -> IO b) -> IO b
如果我们把getLine
放在绑定的左边,我们可以使它更加具体化。
(>>=) :: IO String -> (String -> IO b) -> IO b
好的,所以getLine >>= putStrLn . (++ ". No problem after all!")
getLine >>= putStrLn . (++ ". No problem after all!")
会打印输入的行并添加额外的内容。 右边是一个函数,它接受一个String
并产生一个IO ()
- 这根本不难! 我们只是按类型走。
Monad定义了许多不同的类型,例如Maybe
和[a]
,他们的行为概念相同。
Just 2 >>= return . (+2)
Just 2 >>= return . (+2)
会产生Just 4
,就像你所期望的那样。 请注意,我们必须在这里使用return
,因为否则右侧的函数将不匹配返回类型mb
,而只是b
,这将是一个类型错误。 它在putStrLn的情况下工作,因为它已经产生了一些IO
,这正是我们需要匹配的类型。 (Spoiler:shape of foo >>= return . bar
Monad
是愚蠢的,因为每个Monad
都是Functor
,你能弄清楚它的含义吗?)
我个人认为,只要直觉能够让你了解monad的话题,如果你想深入探索,你确实需要深入理论。 我喜欢首先使用它们。 您可以查看各种Monad实例的源代码,例如Hoogle上的List( []
)Monad或Maybe
Monad,并在精确实现方面稍微精明一些。 一旦你对此感到满意,就可以遵循实际的单子法,并尝试为他们获得更多理论上的理解!
Typeclassopedia有关于Monad
的章节(但是请先阅读关于Functor
和Applicative
的前面章节)。
上一篇: Definition, Laws and Example
下一篇: Is this a monad?