题 什么是monad?


最近简要介绍了Haskell,会是什么 简洁,简洁,实用 关于monad本质上是什么的解释?

我发现我遇到的大多数解释都是相当难以接近的,缺乏实际细节。


1228


起源


Eric Lippert写了这个问题的答案(stackoverflow.com/questions/2704652/...),这是由于一些问题生活在一个单独的页面。 - P Shved
这里的 使用javascript的新介绍 - 我发现它非常易读。 - Benjol
也可以看看 查看monad的不同方式。 - Petr Pudlák
也可以看看 Monads在图片中 - cibercitizen1
monad是一个带辅助操作的函数数组。看到 这个答案 - cibercitizen1


答案:


第一:这个词 单子 如果你不是数学家,那就有点空洞了。另一个术语是 计算建设者 这更像是对它们实际有用的描述。

你问实际的例子:

示例1:列表理解

[x*2 | x<-[1..10], odd x]

此表达式返回1到10范围内所有奇数的双精度数。非常有用!

事实证明,这实际上只是List monad中某些操作的语法糖。相同的列表理解可以写成:

do
   x <- [1..10]
   if odd x 
       then [x * 2] 
       else []

甚至:

[1..10] >>= (\x -> if odd x then [x*2] else [])

示例2:输入/输出

do
   putStrLn "What is your name?"
   name <- getLine
   putStrLn ("Welcome, " ++ name ++ "!")

两个示例都使用monad,AKA计算构建器。共同的主题是monad 连锁经营 以某种特定的,有用的方式。在列表推导中,操作被链接,使得如果操作返回列表,则执行以下操作 每个项目 在列表中。另一方面,IO monad按顺序执行操作,但传递“隐藏变量”,表示“世界状态”,这允许我们以纯函数方式编写I / O代码。

事实证明了这种模式 链接操作 是非常有用的,并用于Haskell中的许多不同的东西。

另一个例子是例外:使用 Error monad,操作被链接以使它们按顺序执行,除非抛出错误,在这种情况下,链的其余部分被放弃。

list-comprehension语法和do-notation都是用于链接操作的语法糖 >>= 运营商。 monad基本上只是一种支持它的类型 >>= 运营商。

示例3:解析器

这是一个非常简单的解析器,它解析带引号的字符串或数字:

parseExpr = parseString <|> parseNumber

parseString = do
        char '"'
        x <- many (noneOf "\"")
        char '"'
        return (StringValue x)

parseNumber = do
    num <- many1 digit
    return (NumberValue (read num))

这些行动 chardigit等等非常简单。它们匹配或不匹配。神奇的是管理控制流程的monad:操作按顺序执行,直到匹配失败,在这种情况下monad回溯到最新 <|> 并尝试下一个选项。同样,一种使用一些额外的,有用的语义来链接操作的方法。

例4:异步编程

上面的例子是在Haskell中,但事实证明 F# 也支持monads。这个例子是从中偷来的 唐赛姆

let AsyncHttp(url:string) =
    async {  let req = WebRequest.Create(url)
             let! rsp = req.GetResponseAsync()
             use stream = rsp.GetResponseStream()
             use reader = new System.IO.StreamReader(stream)
             return reader.ReadToEnd() }

此方法获取网页。打孔线是使用 GetResponseAsync  - 它实际上等待单独线程上的响应,而主线程从函数返回。收到响应后,最后三行在生成的线程上执行。

在大多数其他语言中,您必须为处理响应的行显式创建单独的函数。该 async monad能够自行“拆分”块并推迟后半部分的执行。 (该 async {} 语法表示块中的控制流由 async 单子。)

他们如何工作

那么monad怎么能做所有这些奇特的控制流程呢?在do-block中实际发生了什么(或者 计算表达式 因为它们在F#中被调用,所以每个操作(基本上每一行)都包含在一个单独的匿名函数中。然后使用。组合这些函数 bind 操作员(拼写 >>= 在哈斯克尔)。自从 bind 操作结合了函数,它可以按照它认为合适的方式执行它们:顺序,多次,反向,丢弃一些,在感觉它时在一个单独的线程上执行一些等等。

例如,这是示例2中IO代码的扩展版本:

putStrLn "What is your name?"
>>= (\_ -> getLine)
>>= (\name -> putStrLn ("Welcome, " ++ name ++ "!"))

这是更加丑陋的,但实际上发生的事情也更加明显。该 >>= 运算符是神奇的成分:它需要一个值(在左侧)并将它与一个函数(在右侧)组合,以产生一个新值。然后,下一个采用这个新值 >>= 运算符并再次与函数结合以产生新值。 >>= 可以被视为一个迷你评估者。

注意 >>= 因为不同的类型而重载,所以每个monad都有自己的实现 >>=。 (链中的所有操作都必须是同一个monad的类型,否则就是 >>= 运营商将无法运作。)

最简单的可能实现 >>= 只需取左边的值并将其应用于右边的函数并返回结果,但如前所述,使整个模式有用的是当monad的实现中有一些额外的事情发生时 >>=

值如何从一个操作传递到下一个操作还有一些额外的聪明,但这需要对Haskell类型系统进行更深入的解释。

加起来

在Haskell术语中,monad是一个参数化类型,它是Monad类型的一个实例,它定义了 >>= 以及其他一些运营商。通俗地说,monad只是一种类型的 >>= 操作已定义。

在自身 >>= 只是一种繁琐的链接函数的方式,但是由于存在隐藏“管道”的记号,monadic操作变成了一个非常好的和有用的抽象,在语言的许多地方很有用,并且对于创建很有用你自己的语言迷你语言。

为什么单子难?

对于许多Haskell学习者来说,monad是他们像砖墙一样的障碍。并不是monad本身很复杂,而是实现依赖于许多其他高级Haskell功能,如参数化类型,类型类等。问题是Haskell I / O基于monad,I / O可能是你在学习新语言时想要理解的第一件事 - 毕竟,创建不生成任何语言的程序并不是很有趣输出。我没有立即解决这个鸡蛋问题的问题,除非像对待“魔法发生在这里”那样处理I / O,直到你对语言的其他部分有足够的经验。抱歉。

关于monad的优秀博客: http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html


966



作为一个在理解monad时遇到很多问题的人,我可以说这个答案有点帮助了。但是,还有一些我不理解的事情。列表理解以单子形式表示的是什么?那个例子有扩展形式吗?关于大多数monad解释的另一件事真的困扰我,包括这一点 - 是他们继续混淆“什么是monad?” “什么是monad有益?”和“monad如何实现?”。当你写“Monad基本上只是一个支持>> =运算符的类型时,你跳过那条鲨鱼。”哪个只是让我... - Breton
另外,我不同意你关于为什么monad很难的结论。如果monad本身并不复杂,那么你应该能够在没有一堆行李的情况下解释它们是什么。当我问“什么是monad”这个问题时,我不想知道实现,我想知道它的意思是什么。到目前为止似乎答案是“因为haskell的作者是sadomasochists并决定你应该做一些愚蠢的事情来完成简单的事情,所以你必须学习使用haskell的monad,而不是因为它们在任何方面都有用他们自己”... - Breton
但是......那不可能是对的,可以吗?我认为单子很难,因为没有人能够弄清楚如何解释它们而不会陷入令人困惑的实施细节。我的意思是......什么是校车?它是一个金属平台,前面有一个装置,它消耗一种精炼的石油产品,在一个循环中驱动一些金属活塞,这些金属活塞又转动一个曲轴连接到一些驱动一些轮子的齿轮上。轮子周围有橡胶袋,与橡胶沥青表面相接,使座椅向前移动。座位前进是因为...... - Breton
我阅读了所有这些,但仍然不知道monad是什么,除了Haskell程序员不能很好地解释这个事实。这些例子并没有多大帮助,因为这些都是没有monad可以做的事情,而这个答案并没有说清楚monad如何让它们变得更容易,只会更加混乱。这个答案中接近有用的部分是删除了例子#2的句法糖。我说接近了,因为除了第一行之外,扩展与原始版本没有任何真正的相似之处。 - Laurence Gonsalves
解释monad似乎特有的另一个问题是它是用Haskell编写的。我不是说Haskell是一种糟糕的语言 - 我说这对于解释monad是一种糟糕的语言。如果我知道Haskell我已经理解了monad,那么如果你想解释monad,那么首先要使用一种不懂monad的人更容易理解的语言。如果你 必须 使用Haskell,根本不使用语法糖 - 使用最简单,最简单的语言子集,并且不要假设理解Haskell IO。 - Laurence Gonsalves


解释“什么是monad”有点像说“什么是数字?”我们一直使用数字。但想象你遇到了一个对数字一无所知的人。怎么了 赫克 你会解释一下这些数字是多少?你怎么会开始描述为什么这可能有用呢?

什么是monad?简短的回答:这是将操作链接在一起的一种特定方式。

实质上,您正在编写执行步骤并使用“绑定功能”将它们链接在一起。 (在Haskell中,它被命名为 >>=。)你可以自己编写对bind操作符的调用,或者你可以使用语法sugar,使编译器为你插入这些函数调用。但无论哪种方式,每个步骤都通过调用此绑定函数来分隔。

所以bind函数就像一个分号;它分离了一个过程中的步骤。绑定功能的作用是获取上一步的输出,并将其输入下一步。

这听起来不太难,对吧?但是还有 超过一个 有点monad。为什么?怎么样?

好吧,绑定功能 能够 只需从一步中获取结果,然后将其提供给下一步。但是,如果这是“所有”monad所做的......实际上并不是非常有用。这一点很重要:每一个 有用 monad做了别的事 此外 只是一个单子。一切 有用 monad具有“特殊能力”,这使其独一无二。

(一个monad 没有 特殊称为“身份monad”。更像是身份功能,这听起来像是一个完全没有意义的事情,但结果却不是......但这是另一个故事™。)

基本上,每个monad都有自己的bind函数实现。您可以编写一个绑定函数,以便在执行步骤之间进行连接。例如:

  • 如果每个步骤都返回成功/失败指示符,则只有在前一个步骤成功的情况下,才能让绑定执行下一步。这样,失败的步骤将“自动”中止整个序列,而无需您进行任何条件测试。 (该 蒙纳德失败。)

  • 扩展这个想法,您可以实现“例外”。 (该 错误Monad 要么 Monad。)因为你自己定义它们而不是它是一种语言特性,所以你可以定义它们的工作方式。 (例如,也许你想忽略前两个例外,只有当a 第三 异常被抛出。)

  • 您可以使每个步骤返回 多重结果,并让绑定功能循环在它们上面,为你们提供每一个进入下一步。这样,在处理多个结果时,您不必在整个地方继续编写循环。绑定功能“自动”为您完成所有这些。 (该 列出Monad。)

  • 除了将“结果”从一个步骤传递到另一个步骤之外,您还可以使用绑定功能 传递额外数据 周围也是。此数据现在不会显示在您的源代码中,但您仍然可以从任何地方访问它,而无需手动将其传递给每个函数。 (该 读者莫纳德。)

  • 您可以将其设置为可以替换“额外数据”。这可以让你 模拟破坏性更新,实际上没有进行破坏性更新。 (该 国家Monad 和它的堂兄 作家莫纳德。)

  • 因为你只是 模拟 破坏性的更新,你可以轻而易举地做一些不可能完成的事情 真实 破坏性更新。例如,你可以 撤消上次更新, 要么 恢复到旧版本

  • 你可以制作一个可以计算的monad 暂停,所以你可以暂停你的程序,进入并修补内部状态数据,然后恢复它。

  • 您可以将“continuation”实现为monad。这可以让你 打破人们的思想!

monad可以实现所有这些以及更多。当然,所有这一切也是完全可能的  单子也。这只是戏剧性的 更轻松 使用monads。


638



我很感激你的答案 - 特别是最后的让步,所有这一切当然也可能没有单子。有一点需要做的就是它 大多 使用monad更容易,但它通常不如没有它们那样有效。一旦您需要涉及变换器,函数调用(以及创建的函数对象)的额外分层具有难以查看和控制的成本,通过巧妙的语法使其不可见。 - seh
至少在Haskell中,monad的大部分开销被优化器剥夺了。所以唯一真正的“成本”就是所需的脑力。 (如果“可维护性”是你关心的事情,那么这并非微不足道。)但通常情况下,monad会做出贡献 更轻松,而不是更难。 (否则,你为什么要打扰?) - MathematicalOrchid
来自非数学,非函数式编程背景,这个答案对我来说最有意义。 - jrahhali
这是第一个真正让我知道monad是什么的答案。感谢您找到解释它的方法! - robotmay
你的答案绝对精彩!万分感谢! - Swapnil B.


实际上,与莫纳德的共同理解相反,他们与国家无关。 Monads只是一种包装东西的方法,并提供了对包裹的东西进行操作而无需展开它的方法。

例如,您可以在Haskell中创建一个类型来包装另一个类型:

data Wrapped a = Wrap a

包装我们定义的东西

return :: a -> Wrapped a
return x = Wrap x

要在不展开的情况下执行操作,请说您有一个功能 f :: a -> b,那么你可以这样做 电梯 该函数对包装值起作用:

fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)

这就是所有需要了解的内容。然而,事实证明,有一个更通用的功能来做到这一点 吊装,是的 bind

bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x

bind 可以做多一点 fmap,但反之亦然。其实, fmap 只能用来定义 bind 和 return。因此,在定义monad时...你给它的类型(这里是 Wrapped a)然后说它是怎么回事 return 和 bind 运作工作。

很酷的是,事实证明这是一种普遍的模式,它会在整个地方弹出,以纯粹的方式封装状态只是其中之一。

有关monad如何用于引入函数依赖关系并因此控制评估顺序的好文章,就像在Haskell的IO monad中使用的那样,请查看 IO Inside

至于理解monad,不要太担心它。阅读他们您感兴趣的内容,如果您不立即了解,请不要担心。然后,只需要像Haskell这样的语言潜水就可以了。 Monads是这些通过练习理解涓涓细流到你脑中的事情之一,有一天你突然意识到你理解它们。


164



- >是右关联的镜像函数应用程序,它是左关联的,因此保留括号不会有所不同。 - Matthias Benkard
你的解释对我有用。我会添加一些标准monad(读者,州,也许,......)的有限总和,以说明一些实际用途和包装 - Rabarberski
我认为这根本不是一个很好的解释。 Monads只是一种方式?好吧,哪个方向?为什么我不使用类而不是monad封装? - Breton
对这个想法的更长的解释: blog.sigfpe.com/2007/04/trivial-monad.html - sdcvvc
@ mb21:如果您只是指出支架太多,请注意a-> b-> c实际上只是 - >(b-> c)的缩写。将这个特定的例子写成(a - > b) - >(Ta - > Tb)严格来说只是添加不必要的字符,但它在道德上是“正确的事情”,因为它强调fmap映射类型a的函数 - > b到Ta - > Tb类型的函数。最初,这就是算子在类别理论中所做的事情,也就是monad来自哪里。 - Nikolaj-K


但, 你本可以发明Monads!

sigfpe说:

但是所有这些都将monad引入了一些需要解释的深奥的东西。但我想说的是,它们根本不是深奥的。实际上,面对函数式编程中的各种问题,您将无情地领导某些解决方案,所有解决方案都是monad的示例。事实上,如果你还没有,我希望你现在可以发明它们。然后,这是一个小步骤,注意到所有这些解决方案实际上都是伪装的相同解决方案。读完这篇文章之后,你可能会更好地理解monad上的其他文档,因为你会认识到你所看到的所有你已经发明的东西。

monad试图解决的许多问题都与副作用问题有关。所以我们先从他们开始。 (注意monads让你做的不仅仅是处理副作用,特别是许多类型的容器对象可以被视为monad。对monad的一些介绍发现很难调和monad的这两种不同用法并且只关注一个或者另一个。)

在诸如C ++这样的命令式编程语言中,函数的行为与数学函数完全不同。例如,假设我们有一个C ++函数,它接受一个浮点参数并返回一个浮点结果。从表面上看,它可能看起来有点像数学函数映射实数到实数,但C ++函数可以做的不仅仅是返回一个取决于其参数的数字。它可以读取和写入全局变量的值,也可以将输出写入屏幕并接收来自用户的输入。但是,在纯函数式语言中,函数只能读取其参数中提供给它的内容,并且它对世界产生影响的唯一方法是通过它返回的值。


161



...最好的方式不仅在互联网上,而且在任何地方。 (瓦德勒的原始论文 Monads用于函数式编程 我在下面的答案中提到的也很好。)类比教程中没有一个是类比的。 - ShreevatsaR
这是Sigfpe帖子的JavaScript翻译 对于那些尚未学习高级Haskell的人来说,这是学习monad的新方法! - Sam Watkins
这就是我学习monad的原因。引导读者完成发明概念的过程通常是教授概念的最佳方式。 - Jordan
但是,接受屏幕对象作为参数并返回其修改文本的副本的函数将是纯粹的。 - Dmitri Zaitsev


monad是一种具有两个操作的数据类型: >>= (又名 bind)和 return (又名 unit)。 return 获取任意值并使用它创建monad的实例。 >>= 获取monad的一个实例并在其上映射一个函数。 (您已经可以看到monad是一种奇怪的数据类型,因为在大多数编程语言中,您无法编写一个采用任意值并从中创建类型的函数.Monads使用一种 参数多态性。)

在Haskell表示法中,编写了monad接口

class Monad m where
  return :: a -> m a
  (>>=) :: forall a b . m a -> (a -> m b) -> m b

这些操作应该遵守某些“法律”,但这并不十分重要:“法律”只是编纂了操作的合理实施方式(基本上, >>= 和 return 应该同意如何将值转换为monad实例和那些 >>= 是联想的)。

Monads不只是关于状态和I / O:它们抽象了一种常见的计算模式,包括使用状态,I / O,异常和非确定性。可能最容易理解的monad是列表和选项类型:

instance Monad [ ] where
    []     >>= k = []
    (x:xs) >>= k = k x ++ (xs >>= k)
    return x     = [x]

instance Monad Maybe where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

哪里 [] 和 : 是列表构造函数, ++是连接运算符,和 Just 和 Nothing 是的 Maybe 构造函数。这两个monad都在其各自的数据类型上封装了常见且有用的计算模式(请注意,这两种模式都与副作用或I / O无关)。

你真的必须编写一些非平凡的Haskell代码,以了解monad是什么以及它们为什么有用。


77



“将功能映射到它上面”究竟是什么意思? - Casebash
Casebash,我在介绍中故意非正式。查看末尾附近的示例,以了解“映射函数”需要什么。 - Chris Conway
Monad不是数据类型。这是一个组成功能的规则: stackoverflow.com/a/37345315/1614973 - Dmitri Zaitsev


你应该先了解一个仿函数是什么。在此之前,了解高阶函数。

一个 高阶函数 只是一个将函数作为参数的函数。

一个 函子 是任何类型的结构 T 对于哪个存在高阶函数,请调用它 map,转换类型的功能 a -> b (给出任何两种类型 a 和 b成功能 T a -> T b。这个 map 功能还必须遵守身份和组成的规律,以便以下表达式对所有人都适用 p 和 q (Haskell表示法):

map id = id
map (p . q) = map p . map q

例如,一个名为的构造函数 List 如果它配备了类型的功能,它是一个仿函数 (a -> b) -> List a -> List b 遵守上述法律。唯一实际的实施是显而易见的。所结果的 List a -> List b 函数遍历给定列表,调用 (a -> b) 每个元素的函数,并返回结果列表。

一个 单子 基本上只是一个算符 T 有两个额外的方法, join,类型 T (T a) -> T a,和 unit (有时称为 returnfork, 要么 pure)类型 a -> T a。对于Haskell中的列表:

join :: [[a]] -> [a]
pure :: a -> [a]

为什么这有用?因为你可以,例如, map 在具有返回列表的函数的列表上。 Join 获取结果列表并连接它们。 List 是monad,因为这是可能的。

你可以编写一个功能 map, 然后 join。调用此函数 bind, 要么 flatMap, 要么 (>>=), 要么 (=<<)。这通常是在Haskell中给出monad实例的方式。

monad必须满足某些法则,即那些 join 必须是联想的。这意味着如果你有一个值 x 类型 [[[a]]] 然后 join (join x) 应该相等 join (map join x)。和 pure 必须是身份 join 这样的 join (pure x) == x


71



稍微增加'更高阶函数'的def:它们可以采用OR RETURN函数。这就是为什么他们“更高”,因为他们自己做事。 - Kevin Won
根据该定义,加法是高阶函数。它需要一个数字并返回一个将该数字添加到另一个数字的函数。所以不,高阶函数是严格的函数,其域由函数组成。 - Apocalisp
视频 'Brian Beckman:不要害怕Monad'遵循同样的逻辑。 - icc97


[免责声明:我仍然试图完全修改单子。以下是我到目前为止所理解的内容。如果这是错的,希望知识渊博的人会叫我在地毯上。]

阿纳写道:

Monads只是一种包装东西的方法,并提供了对包裹的东西进行操作而无需展开它的方法。

这正是它。这个想法是这样的:

  1. 你需要一些价值,并附上一些额外的信息。就像值是某种类型(例如,整数或字符串)一样,附加信息也是某种类型。

    例如,额外的信息可能是一个 Maybe 或者 IO

  2. 然后你有一些操作符,允许你在携带附加信息的同时操作包装数据。这些运算符使用附加信息来决定如何更改包装值上的操作行为。

    例如,a Maybe Int 可以是一个 Just Int 要么 Nothing。现在,如果你添加一个 Maybe Int 到了 Maybe Int,操作员将检查它们是否都是 Just Int在里面,如果是的话,将解开 Ints,将它们传递给加法运算符,重新包装结果 Int 变成新的 Just Int (这是有效的 Maybe Int),从而返回一个 Maybe Int。但如果他们中的一个是 Nothing 在里面,这个操作员将立即返回 Nothing,这也是有效的 Maybe Int。那样,你可以假装你的 Maybe Ints只是正常数字,并对它们进行常规数学运算。如果你得到一个 Nothing,你的方程式仍将产生正确的结果 - 没有你必须乱丢垃圾 Nothing 到处

但这个例子正是发生了什么 Maybe。如果额外的信息是 IO,然后定义的特殊运算符 IOs会被调用,它可以在执行添加之前做一些完全不同的事情。 (好的,加两个 IO Int在一起可能是荒谬的 - 我还不确定。)(另外,如果你注意到了 Maybe 例如,您已经注意到“使用额外的东西包装值”并不总是正确的。但是,如果不是不可理解的话,很难准确,正确和准确。)

基本上, “monad”大致意为“模式”。但是现在你没有一本充满非正式解释和特别命名的模式的书 语言结构  - 语法和所有 - 允许你 将新模式声明为程序中的事物。 (这里的不精确是所有的模式必须遵循一种特定的形式,所以monad并不像模式那样通用。但我认为这是大多数人都知道和理解的最接近的术语。)

这就是为什么人们发现monad如此混乱:因为它们是如此通用的概念。要问是什么使monad成为同样含糊不清的问题是什么使某事成为一种模式。

但是想一想在语言中使用句法支持对于模式的概念的含义:而不是必须阅读 四人帮 你只是预订并记住特定模式的构建 编写以不可知的通用方式实现此模式的代码 一旦你完成了!然后,您可以重复使用此模式,例如Visitor或Strategy或Façade等等,只需通过使用它来装饰代码中的操作,而无需反复重复实现它!

那就是为什么人们 理解 monads找到了他们 有用:这不是一些象牙塔的概念,知识分子自豪于理解(好吧,当然,teehee),但实际上使代码更简单。


42



有时来自“学习者”(像你一样)的解释与另一个学习者的关系比来自专家的解释更有意义。学习者认为相似:) - Adrian
是什么使monad成为存在类型的函数 M (M a) -> M a。您可以将其转换为类型之一 M a -> (a -> M b) -> M b 是什么使他们有用。 - Jeremy List
“monad”大致意味着“模式”......不。 - user633183