Monads - 定义,法律和例子
Posted
技术标签:
【中文标题】Monads - 定义,法律和例子【英文标题】:Monads - Definition, Laws and Example [duplicate] 【发布时间】:2012-08-24 06:19:51 【问题描述】:可能重复:What is a monad?
我正在学习使用 Haskell 的函数式语言进行编程,并且在学习解析器时遇到了 Monads。我以前从未听说过它们,所以我做了一些额外的研究来找出它们是什么。
为了学习这个主题,我到处寻找都让我感到困惑。我真的找不到关于 Monad 是什么以及如何使用它们的简单定义。 “单子是一种根据值和使用这些值的计算序列来构造计算的方法” - 嗯???
有人可以提供一个简单的定义,什么是 Haskell 中的 Monad,与它们相关的法律并举个例子吗?
注意:我知道如何使用 do 语法,因为我已经了解了具有副作用的 I/O 操作和函数。【问题讨论】:
那里有很多很好的教程。一个例子是learnyouahaskell.com/a-fistful-of-monads,也许你应该先阅读关于函子的章节。另外,请列出您已经阅读和不理解的资源 ***.com/questions/44965/what-is-a-monad 以下是我个人的拙见。你真的,真的需要深入了解范畴论中的单子是什么,包括它与伴随物的联系。大多数面向 Haskell 的教程几乎没有涉及到这一点,这使得它们相当混乱和毫无意义。抱歉,编程很复杂,没办法。 @n.m.你需要深入理解范畴论层面的单子做什么?如果你想对编程语言进行新的研究,我同意。如果您想使用 monad 编写简洁的程序来构建您的计算,我强烈不同意。 @n.m.说“你需要深入理解什么是单子......以了解你真正在做什么[当你使用单子时]”有点重言式。我认为在使用现有的单子库时它也没有多大帮助,除非您指的是通用单子操作库(例如转换器、Control.Monad 之类的东西)。为此,当然还要创建新的 monad,我同意这很有帮助。 【参考方案1】:直觉
粗略的直觉是,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 的实例。与 fmap 等效的操作在 Monad 库中称为 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
在设计自己的 Monad 时,您可能会发现更容易,而不是尝试直接定义 (>>=)
,而是将问题拆分为多个部分,并弄清楚如何映射和连接您的结构。拥有 map 和 join 也可以用于验证您的 Monad 是否定义良好,即它是否满足所需的法律。
单子定律
你的 Monad 应该是一个 Functor,所以映射操作应该满足:
fmap id = id
fmap g . fmap f = fmap (g . f)
return 和 join 的规律是:
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)
【讨论】:
我认为这个答案有点儿爱...基于join
的法律让我很开心,谢谢:)【参考方案2】:
Typeclassopedia 有一个关于Monad
的部分(但请先阅读前面关于Functor
和Applicative
的部分)。
【讨论】:
【参考方案3】:Haskell 中的 monad 定义了两个操作:
(>>=) :: Monad m => m a -> (a -> m b) -> m b -- also called bind
return :: Monad m => a -> m a
这两个运算需要满足某些定律,如果你不具备数学思维的诀窍,这可能会让你在这一点上感到困惑。从概念上讲,您使用 bind 在单子级别上操作值并返回以从“微不足道”的值创建单子值。例如,
getLine :: IO String
,
所以你不能修改和putStrLn
这个String
——因为它不是String
而是IO String
!
好吧,我们手头有一个 IO Monad,所以不用担心。我们所要做的就是使用 bind 来做我们想做的事。让我们看看 IO Monad 中的 bind 是什么样子的:
(>>=) :: IO a -> (a -> IO b) -> IO b
如果我们将getLine
放在bind 的左侧,我们可以使它更具体。
(>>=) :: IO String -> (String -> IO b) -> IO b
好的,所以getLine >>= putStrLn . (++ ". No problem after all!")
将打印输入的行并添加了额外的内容。右手边是一个函数,它接受 String
并产生 IO ()
- 这一点都不难!我们只是按类型。
为许多不同的类型定义了 Monad,例如 Maybe
和 [a]
,它们在概念上的行为方式相同。
Just 2 >>= return . (+2)
将产生Just 4
,正如您所料。请注意,我们必须在这里使用return
,否则右侧的函数将不匹配返回类型m b
,而是匹配b
,这将是一个类型错误。它在 putStrLn 的情况下工作,因为它已经产生了一个 IO
的东西,这正是我们的类型需要匹配的。 (剧透:foo >>= return . bar
形状的表达很愚蠢,因为每个Monad
都是Functor
。你能弄清楚这是什么意思吗?)
我个人认为,就直觉而言,这只是让您了解单子的主题,如果您想更深入地研究,您确实需要深入研究该理论。我喜欢先使用它们。您可以在 Hoogle 上查找各种 Monad 实例的源代码,例如 List ([]
) Monad 或 Maybe
Monad,并在具体实现上更加智能。一旦您对此感到满意,请尝试实际的单子定律并尝试对它们进行更理论的理解!
【讨论】:
我想补充一点,顺便说一句,这确实不算作 Monad 教程,所以当谈到个人 Monad 时,我仍然处于零教程。以上是关于Monads - 定义,法律和例子的主要内容,如果未能解决你的问题,请参考以下文章