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 的部分(但请先阅读前面关于FunctorApplicative 的部分)。

【讨论】:

【参考方案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 - 定义,法律和例子的主要内容,如果未能解决你的问题,请参考以下文章

聊聊代理

各国比特币的政策

爬虫技术涉案大数据分析及法律解读

实务研究 | 爬虫技术涉案大数据分析及法律解读

“法治尉氏”公众号开通国家法律法规数据库查询通道

二叉树聊“行商民”法律法规及规章-行政许可法1