什么是monad?

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

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

310366 次浏览

但是你本可以发明单子的!

sigfpe说:

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

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

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

我最喜欢的Monad教程:

http://www.haskell.org/haskellwiki/All_About_Monads

(在谷歌搜索“monad教程”的170,000次点击中!)

@Stu:monads的意义在于允许你为纯代码添加(通常是)顺序语义学;你甚至可以编写monad(使用Monad Trans的),并获得更有趣和复杂的组合语义学,例如解析与错误处理,共享状态和日志记录。所有这些都可以在纯代码中实现,monads只是允许你将其抽象出来并在模块化库中重用它(在编程中总是很好),以及提供方便的语法使其看起来势在必行。

Haskell已经有了运算符重载[1]:它使用类型类的方式与Java或C#中使用接口的方式非常相似,但Haskell恰好也允许+&&和>等非字母数字标记作为中缀标识符。如果你的意思是“重载分号”[2],这只是运算符重载。“重载分号”听起来像是黑魔法和自找麻烦(图片中有进取心的Perl黑客听到了这个想法),但关键是没有monads没有分号,因为纯函数代码不需要或不允许显式排序。

sigfpe的文章非常酷,但使用Haskell来解释它,这有点无法打破理解Haskell到grok Monads和理解Monads到grok Haskell的鸡和蛋问题。

[1]这是与monad不同的问题,但monad使用Haskell的运算符重载功能。

[2]这也是一种过度简化,因为链接一元操作的运算符是>>=(发音为“bind”),但有语法糖(“do”)允许您使用大括号和分号和/或缩进和换行符。

monad实际上是“类型运算符”的一种形式。它将做三件事。首先,它将“包装”(或以其他方式转换)一种类型的值到另一种类型(通常称为“monadic类型”)。其次,它将使基础类型上可用的所有操作(或函数)在monadic类型上可用。最后,它将支持将其自身与另一个monad组合以产生复合monad。

“也许monad”本质上相当于Visual Basic/C#中的“可空类型”。它接受一个不可为空的类型“T”并将其转换为“可为空”,然后定义所有二元运算符在Nullable上的含义。

副作用以类似的方式表示。创建一个结构,其中包含副作用的描述以及函数的返回值。然后,当值在函数之间传递时,“提升”操作会复制副作用。

它们被称为“monad”,而不是“类型运算符”的更容易掌握的名称,原因有几个:

  1. 单子对它们能做什么有限制(详见定义)。
  2. 这些限制,以及涉及三个操作的事实,符合范畴理论中称为monad的结构,这是数学的一个模糊分支。
  3. 它们是由“纯”函数式语言的支持者设计的
  4. 纯函数式语言的支持者喜欢晦涩难懂的数学分支
  5. 因为数学是晦涩难懂的,而且monad与特定的编程风格有关,人们倾向于使用monad这个词作为一种秘密握手。正因为如此,没有人费心去投资一个更好的名字。

monad是用于封装状态不断变化的对象的东西。它最常见于不允许您拥有可修改状态的语言(例如,Haskell)。

一个例子是文件I/O。

您可以使用monad进行文件I/O来将不断变化的状态性质与使用Monad的代码隔离开来。Monad内部的代码可以有效地忽略Monad外部世界不断变化的状态-这使得更容易推理程序的整体效果。

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

在Haskell符号中,monad接口被编写为

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

这些操作应该遵守某些“定律”,但这并不重要:“定律”只是编纂了操作的合理实现应该表现的方式(基本上,>>=return应该就值如何转换为monad实例达成一致,>>=是关联的)。

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

instance Monad [ ] where[]     >>= k = [](x:xs) >>= k = k x ++ (xs >>= k)return x     = [x]
instance Monad Maybe whereJust x  >>= k = k xNothing >>= k = Nothingreturn x      = Just x

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

你真的需要编写一些重要的Haskell代码来理解monad是关于什么的以及它们为什么有用。

(另见What is a monad?

一个很好的动机是sigfpe(丹·皮波尼)的你可能已经发明了单子!(也许你已经有了)。有很多其他monad教程,其中许多人错误地试图用各种类比用“简单的术语”解释monad:这是monad教程谬误;避免它们。

正如DR MacIver在告诉我们为什么你的语言很烂中所说:

所以,我讨厌Haskell的事情:

让我们从显而易见的开始。Monad教程。不,不是monad。特别是教程。它们无休止,夸大了,亲爱的上帝,它们是乏味的。此外,我从未见过任何令人信服的证据表明它们实际上有帮助。阅读类定义,写一些代码,克服可怕的名字。

你说你理解也许单子?很好,你在你的路上。只要开始使用其他单子,迟早你会明白单子一般是什么。

[如果你是面向数学的,你可能想忽略几十个教程并学习定义,或者按照范畴理论讲座:)定义的主要部分是Monad M涉及一个“类型构造函数”,该构造函数为每个现有类型“T”定义一个新类型“M T”,以及在“常规”类型和“M”类型之间来回切换的一些方法。]

而且,令人惊讶的是,关于monads的最好的介绍之一实际上是早期的学术论文之一,介绍monads,Philip Wadler的函数式编程的单子。它实际上有实用的,非平凡的激励例子,不像许多人为的教程。

如果我的理解正确的话,IENumable是从monad派生出来的。我想知道对于我们这些来自C#世界的人来说,这是否是一个有趣的方法角度?

为了它的价值,这里有一些帮助我的教程链接(不,我仍然不明白monad是什么)。

实际上,与通常对Monad的理解相反,它们与状态无关。Monad只是一种包装事物的方法,并提供在不打开包装的情况下对包装的事物进行操作的方法。

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

data Wrapped a = Wrap a

包装我们定义的东西

return :: a -> Wrapped areturn 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只能根据bindreturn来定义。所以,当定义一个monad…时,你给出它的类型(这里是Wrapped a),然后说明它的returnbind操作是如何工作的。

很酷的是,这是一个如此普遍的模式,以至于它到处都是,以纯粹的方式封装状态只是其中之一。

有关如何使用monad引入函数依赖关系从而控制评估顺序的好文章,就像在Haskell的IO monad中使用的一样,请查看IO内部

至于理解单子,不要太担心。阅读你觉得有趣的东西,如果你不能马上理解,也不要担心。然后,深入学习像Haskell这样的语言就是要走的路。单子是通过练习慢慢进入你大脑的东西之一,有一天你会突然意识到你理解了它们。

【免责声明:我还在尝试完全理解monads。以下是我目前所了解的。如果错了,希望有知识渊博的人会在地毯上打电话给我。】

Arnar写道:

单子只是一种包装东西的方法,并提供在不打开包装的情况下对包装的东西进行操作的方法。

正是如此。这个想法是这样的:

  1. 你获取某种值并用一些附加信息包装它。就像值是某种类型的(例如。整数或字符串),所以附加信息是某种类型的。

    例如,额外的信息可能是MaybeIO

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

    例如,Maybe Int可以是Just IntNothing。现在,如果你将Maybe Int添加到Maybe Int,运算符将检查它们是否都在内部的Just Int,如果是,将解开Int,传递给它们加法运算符,将结果Int重新包装成一个新的Just Int(这是一个有效的Maybe Int),从而返回一个Maybe Int。但是如果其中一个是内部的Nothing,这个运算符将立即返回Nothing,这也是一个有效的Maybe Int。这样,你可以假装你的Maybe Int只是普通数字,并对它们执行常规数学。如果你得到了Nothing,你的方程式仍然会产生正确的结果-Just Int7。

但这个例子正是Maybe的情况。如果额外的信息是IO,那么为IOs定义的特殊运算符将被调用,它可以在执行加法之前做一些完全不同的事情。(好吧,将两个IO Int加在一起可能是荒谬的——我还不确定。)(此外,如果你注意了Maybe的例子,你会注意到“用额外的东西包装一个值”并不总是正确的。但很难做到准确、正确和精确,而不会让人难以理解。)

基本上,"monad"大致意思是"模式"。但是,现在你有了一种语言结构——语法和所有——这让你可以将新模式声明为程序中的事物,而不是一本满是非正式解释和专门命名的模式的书。(这里的不精确之处在于,所有的模式都必须遵循一种特定的形式,所以monad并不像模式那样通用。但我认为这是大多数人知道和理解的最接近的术语。)

这就是为什么人们觉得monad如此令人困惑的原因:因为monad是一个非常通用的概念。问什么使某物成为monad就像问什么使某物成为模式一样含糊不清。

但是想想在语言中为模式的概念提供语法支持的含义:不必阅读四人帮书并记住特定模式的构造,你只需编写以不可知论的通用方式实现此模式的代码一次,然后你就完成了!然后你可以重用这个模式,就像访问者、策略或外观或其他什么,只需用它装饰代码中的操作,而不必一遍又一遍地重新实现它!

所以这就是为什么那些理解 monads的人发现它们如此有用:这不是知识势利者以理解为傲的象牙塔概念(好吧,当然,teehee),但实际上使代码更简单。

您应该首先了解什么是仿函数。在此之前,了解高阶函数。

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

仿函数是任何类型构造T,其中存在一个高阶函数,称之为map,它将类型a -> b的函数(给定任意两个类型ab)转换为函数T a -> T b。这个map函数还必须遵守恒等和组合定律,以便以下表达式对所有pq返回true(Haskell表示法):

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

例如,一个名为List的类型构造函数是一个仿函数,如果它配备了一个符合上述定律的(a -> b) -> List a -> List b类型的函数。唯一实际的实现是显而易见的。生成的List a -> List b函数迭代给定的列表,为每个元素调用(a -> b)函数,并返回结果列表。

monad本质上只是一个仿函数T,有两个额外的方法,#1,类型T (T a) -> T aunit(有时称为returnforkpure),类型a -> T a。对于Haskell中的列表:

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

为什么这很有用?因为你可以,例如,使用返回列表的函数在列表上mapJoin获取结果列表并将它们连接起来。List是一个单子,因为这是可能的。

您可以编写一个执行map然后join的函数。此函数称为bindflatMap(>>=)(=<<)。这通常是Haskell中给出monad实例的方式。

monad必须满足某些定律,即join必须是关联的。这意味着如果你有[[[a]]]类型的值x,那么join (join x)应该等于join (map join x)pure必须是join的标识,这样join (pure x) == x

首先:如果你不是数学家,术语monad有点空洞。另一个术语是计算生成器,它更多地描述了它们实际有用的东西。

它们是链接操作的模式。它看起来有点像面向对象语言中的方法链接,但机制略有不同。

该模式主要用于函数式语言(尤其是Haskell,它普遍使用monad),但可以用于任何支持高阶函数(即可以将其他函数作为参数的函数)的语言。

JavaScript中的数组支持该模式,因此让我们将其用作第一个示例。

模式的要点是我们有一个类型(在本例中为Array),它有一个接受函数作为参数的方法。提供的操作必须返回相同类型的实例(即返回Array)。

首先是一个方法链的例子,它没有使用monad模式:

[1,2,3].map(x => x + 1)

结果是[2,3,4]。代码不符合monad模式,因为我们作为参数提供的函数返回一个数字,而不是一个数组。monad形式的相同逻辑是:

[1,2,3].flatMap(x => [x + 1])

这里我们提供了一个返回Array的操作,所以现在它符合模式。#1方法为数组中的每个元素执行提供的函数。它期望每次调用都有一个数组作为结果(而不是单个值),但将结果数组集合并为一个数组。所以最终结果是一样的,数组[2,3,4]

(提供给mapflatMap等方法的函数参数在JavaScript中通常称为“回调”。我将其称为“操作”,因为它更通用。)

如果我们链接多个操作(以传统方式):

[1,2,3].map(a => a + 1).filter(b => b != 3)

数组中的结果[2,4]

Monad形式的相同链接:

[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])

产生相同的结果,即数组[2,4]

你会立即注意到monad形式比非monad形式要丑得多!这只是表明monad不一定是“好的”。它们是一种模式,有时是有益的,有时不是。

请注意,monad模式可以以不同的方式组合:

[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))

这里的绑定是嵌套的而不是链接的,但结果是一样的。这是monad的一个重要属性,我们将在后面看到。这意味着两个操作的组合可以被视为单个操作。

允许该操作返回具有不同元素类型的数组,例如将数字数组转换为字符串数组或其他内容;只要它仍然是一个数组。

这可以使用TypeScript表示法更正式地描述。数组的类型为Array<T>,其中T是数组中元素的类型。方法flatMap()接受类型为T => Array<U>的函数参数并返回Array<U>

一般来说,monad是任何类型Foo<Bar>,它有一个“bind”方法,它接受类型Bar => Foo<Baz>的函数参数并返回Foo<Baz>

这个答案什么 monad是。这个答案的其余部分将尝试通过示例解释为什么monad在像Haskell这样对它们有很好支持的语言中是一个有用的模式。

Haskell和Do-noation

要将map/filter示例直接转换为Haskell,我们将flatMap替换为>>=运算符:

[1,2,3] >>= \a -> [a+1] >>= \b -> if b == 3 then [] else [b]

>>=运算符是Haskell中的绑定函数。当操作数是列表时,它与JavaScript中的flatMap相同,但对于其他类型,它重载了不同的含义。

但是Haskell也有一个用于monad表达式的专用语法,do-block,它完全隐藏了绑定运算符:

 do a <- [1,2,3]b <- [a+1]if b == 3 then [] else [b]

这隐藏了“管道”,让您专注于每个步骤应用的实际操作。

do块中,每一行都是一个操作。约束仍然保持块中的所有操作必须返回相同的类型。由于第一个表达式是列表,因此其他操作也必须返回列表。

反箭头<-看起来像一个赋值,但请注意,这是在绑定中传递的参数。因此,当右侧的表达式是整数列表时,左侧的变量将是一个整数-但将对列表中的每个整数执行。

示例:安全导航(可能类型)

关于列表,让我们看看monad模式如何对其他类型有用。

某些函数可能并不总是返回一个有效的值。在Haskell中,这由Maybe类型表示,这是一个Just valueNothing的选项。

总是返回有效值的链接操作当然很简单:

streetName = getStreetName (getAddress (getUser 17))

但是如果任何函数都可以返回Nothing怎么办?我们需要单独检查每个结果,只有当它不是Nothing时才将值传递给下一个函数:

case getUser 17 ofNothing -> NothingJust user ->case getAddress user ofNothing -> NothingJust address ->getStreetName address

相当多的重复检查!想象一下如果链更长。Haskell用Maybe的monad模式解决了这个问题:

douser <- getUser 17addr <- getAddress usergetStreetName addr

这个do块调用Maybe类型的bind函数(因为第一个表达式的结果是Maybe)。bind函数仅在值为Just value时执行以下操作,否则它只会传递Nothing

这里使用monad模式来避免重复代码。这类似于其他一些语言使用宏来简化语法的方式,尽管宏以非常不同的方式实现相同的目标。

请注意,它是monad模式的组合和Haskell中monad友好的语法,这导致代码更清晰。在像JavaScript这样没有任何对monad的特殊语法支持的语言中,我怀疑monad模式在这种情况下能够简化代码。

可变状态

Haskell不支持可变状态。所有变量都是常量,所有值都是不可变的。但State类型可用于模拟具有可变状态的编程:

add2 :: State Integer Integeradd2 = do-- add 1 to statex <- getput (x + 1)-- increment in another waymodify (+1)-- return stateget

evalState add2 7=> 9

add2函数构建一个monad链,然后以7作为初始状态进行评估。

显然,这只有在Haskell中才有意义。其他语言开箱即用地支持可变状态。Haskell通常对语言特性进行“选择加入”——您在需要时启用可变状态,类型系统确保效果是明确的。IO是另一个例子。

IO

IO类型用于链接和执行“不纯”函数。

像任何其他实用语言一样,Haskell有一堆与外部世界接口的内置函数:putStrLinereadLine等。这些函数被称为“不纯”,因为它们要么会引起副作用,要么结果不确定。即使是像获取时间这样简单的东西也被认为是不纯的,因为结果是不确定的——用同样的参数调用它两次可能会返回不同的值。

纯函数是确定性的——它的结果纯粹取决于传递的参数,除了返回值之外,它对环境没有副作用。

Haskell非常鼓励使用纯函数——这是该语言的一个主要卖点。不幸的是,对于纯粹主义者来说,你需要一些不纯函数来做任何有用的事情。Haskell的妥协是将纯函数和不纯函数清晰地分开,并保证纯函数不可能直接或间接执行不纯函数。

这是通过赋予所有不纯函数IO类型来保证的。Haskell程序中的切入点是具有IO类型的main函数,因此我们可以在顶层执行不纯函数。

但是该语言如何防止纯函数执行不纯函数呢?这是由于Haskell的惰性。一个函数只有在其输出被其他函数消费时才会被执行。但是除了将IO值赋给main之外,没有办法消费它。所以如果一个函数想执行一个不纯函数,它必须连接到main并具有IO类型。

对IO操作使用monad链还可以确保它们以线性和可预测的顺序执行,就像命令式语言中的语句一样。

这给我们带来了大多数人会用Haskell编写的第一个程序:

main :: IO ()main = doputStrLn ”Hello World”

当只有一个操作时,do关键字是多余的,因此没有任何绑定,但我还是保留它以保持一致性。

()类型表示“无效”。这种特殊的返回类型仅对为其副作用调用的IO函数有用。

一个更长的例子:

main = doputStrLn "What is your name?"name <- getLineputStrLn "hello" ++ name

这构建了一个IO操作链,由于它们被分配给main函数,它们被执行。

IOMaybe进行比较可以看出monad模式的多功能性。对于Maybe,该模式用于通过将条件逻辑移动到绑定函数来避免重复代码。对于IO,该模式用于确保IO类型的所有操作都是有序的,并且IO操作不会“泄漏”给纯函数。

总结

在我的主观观点中,monad模式只有在对模式有内置支持的语言中才是真正有价值的。否则,它只会导致过于复杂的代码。但是Haskell(和其他一些语言)有一些内置支持,隐藏了乏味的部分,然后该模式可以用于各种有用的事情。比如:

  • 避免重复代码(Maybe
  • 为程序的分隔区域添加可变状态或异常等语言功能。
  • 从好东西中分离出讨厌的东西(IO
  • 嵌入式领域特定语言(Parser
  • 将GOTO添加到语言中。

最近,我一直在以不同的方式思考单子。我一直在思考它们以数学的方式抽象出执行令,这使得新的多态性成为可能。

如果您使用的是命令式语言,并且您按顺序编写了一些表达式,则代码始终按该顺序运行。

在简单的情况下,当你使用monad时,感觉是一样的——你定义了一个按顺序发生的表达式列表。不同的是,根据你使用的monad,你的代码可能会按顺序运行(如IO monad),同时并行运行多个项目(如List monad),它可能会中途停止(如“可能”monad),它可能会暂停中途以稍后恢复(如“恢复”monad),它可能会倒带并从头开始(如“事务”monad),或者它可能会中途倒带以尝试其他选项(如“逻辑”monad)。

由于monad是多态的,因此可以在不同的monad中运行相同的代码,具体取决于您的需求。

另外,在某些情况下,可以将monad组合在一起(与monad变压器)以同时获得多个功能。

除了上面的优秀答案之外,让我为您提供以下文章的链接(由Patrick Thomson撰写),该文章通过将概念与JavaScript库jQuery(及其使用“方法链”来操作DOM的方式)相关联来解释monads:jQuery是一个单子

jQuery留档本身并没有提到术语“monad”,而是谈到了可能更熟悉的“构建器模式”。这并没有改变你在那里有一个合适的monad的事实,也许你甚至没有意识到这一点。

在了解那里时,对我帮助最大的两件事是:

第8章,“函数解析器”,摘自Graham Hutton的书在Haskell中编程。实际上,这根本没有提到monad,但是如果你能通读这一章并真正理解其中的所有内容,特别是如何评估一系列绑定操作,你就会理解monad的内部。预计这需要几次尝试。

教程关于Monads的一切。这给出了它们使用的几个很好的例子,我不得不说,Appendx中的类比我为我工作。

单子用于控制流,就像抽象数据类型对于数据一样。

换句话说,许多开发人员对集合、列表、字典(或哈希值或映射)和树的概念感到满意。在这些数据类型中,有许多特殊情况(例如InsertionOrderP保留IdtyHashMap)。

然而,当面对程序“流”时,许多开发人员并没有接触到比if、Switch/case、do、这时候、goto(grr)和(可能)闭包更多的结构。

所以,monad仅仅是一个控制流结构。一个更好的短语来代替monad是“控制类型”。

因此,monad有用于控制逻辑、语句或函数的插槽——数据结构中的等价物是说某些数据结构允许您添加数据和删除数据。

例如,“if”monad:

if( clause ) then block

最简单的是有两个槽——一个子句和一个块。if monad通常用于评估子句的结果,如果不是false,则评估块。许多开发人员在学习“if”时没有了解monad,只是没有必要理解monad来编写有效的逻辑。

单子可以变得更加复杂,就像数据结构可以变得更加复杂一样,但是有许多广泛的单子类别可能具有相似的语义学,但不同的实现和语法。

当然,就像可以迭代或遍历数据结构一样,可以评估单子。

编译器可能支持也可能不支持用户定义的monad。Haskell当然支持。Ioke有一些类似的功能,尽管该语言中没有使用monad一词。

公主F#计算表达式的解释帮助了我,尽管我仍然不能说我真的理解了。

编辑:这个系列-用javascript解释monads-对我来说是“平衡”的一个。

我认为理解单子是一件让你毛骨悚然的事情。从这个意义上说,尽可能多地阅读教程是个好主意,但通常奇怪的东西(不熟悉的语言或语法)会让你的大脑无法专注于基本的东西。

有些事情我很难理解:

  • 基于规则的解释对我来说从来都不起作用,因为大多数实际示例实际上需要更多而不仅仅是返回/绑定。
  • 此外,称它们为规则也没有帮助。这更像是“有这些事情有一些共同点,让我们称这些东西为'monad',而共同的比特为'规则'”。
  • 返回(a -> M<a>)和绑定(M<a> -> (a -> M<b>) -> M<b>)都很棒,但我永远无法理解的是绑定如何从M<a>中提取a以将其传递到a -> M<b>。我想我从未在任何地方读过(也许对其他人来说很明显),返回(M<a> -> a已经的反面存在里面单子,它只是不需要暴露。

http://code.google.com/p/monad-tutorial/是解决这个问题的半成品。

monad是一种将共享公共上下文的计算组合在一起的方式。这就像构建一个管道网络。在构建网络时,没有数据流过它。但是当我用'bind'和'返回'将所有位拼凑在一起时,然后我调用类似runMyMonad monad data的东西,数据流过管道。

经过一番努力,我想我终于理解了单子。在重读了我自己对绝大多数投票最多的答案的冗长批评后,我将提供这个解释。

有三个问题需要回答才能理解monad:

  1. 你为什么需要单子?
  2. 什么是monad?
  3. monad是如何实现的?

正如我在最初的评论中指出的,太多的单子解释被问题3所困扰,没有,在真正充分覆盖问题2或问题1之前。

你为什么需要单子?

像Haskell这样的纯函数式语言与C或Java这样的命令式语言不同,因为纯函数式程序不一定按照特定的顺序执行,一次执行一步。Haskell程序更类似于数学函数,你可以在其中以任意数量的潜在顺序求解“方程”。这带来了许多好处,其中之一是它消除了某些类型错误的可能性,特别是与“状态”之类的东西相关的错误。

然而,有一些问题不是用这种编程风格解决的那么简单。有些事情,比如控制台编程和文件i/o,需要事情以特定的顺序发生,或者需要维护状态。处理这个问题的一种方法是创建一种表示计算状态的对象,以及一系列以状态对象为输入,并返回一个新的修改后的状态对象的函数。

因此,让我们创建一个假设的“状态”值,它表示控制台屏幕的状态。这个值的构造方式并不重要,但假设它是一个字节长度的ascii字符数组,表示当前在屏幕上可见的内容,以及一个表示用户输入的最后一行输入的数组,在伪代码中。我们定义了一些接受控制台状态、修改它并返回新控制台状态的函数。

consolestate MyConsole = new consolestate;

因此,要进行控制台编程,但以纯函数方式,您需要在彼此之间嵌套大量函数调用。

consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");

以这种方式编程保持了“纯”函数式风格,同时强制以特定顺序对控制台进行更改。但是,我们可能希望一次做的不仅仅是几个操作,就像上面的例子一样。以这种方式嵌套函数将开始变得笨拙。我们想要的是做与上面基本相同的事情的代码,但编写得更像这样:

consolestate FinalConsole = myconsole:print("Hello, what's your name?"):input():print("hello, %inputbuffer%!");

这确实是一种更方便的写法。但是我们怎么做呢?

什么是monad?

一旦你有了一个类型(比如consolestate),你定义了一堆专门针对该类型设计的函数,你可以通过定义一个像:(bind)这样的运算符,将这些东西的整个包变成一个“monad”,它自动将左边的返回值提供给右边的函数参数,以及一个lift运算符,它将普通函数转换为与特定类型的绑定运算符一起工作的函数。

monad是如何实现的?

看看其他答案,这似乎很自由地跳入细节。

单子不是隐喻,但正如Daniel Spiewak所解释的那样,这是一个从通用模式中出现的实际有用的抽象。

解释monad似乎就像解释控制流语句。想象一下,一个非程序员要求你解释它们?

你可以给他们一个涉及理论的解释——布尔逻辑、寄存器值、指针、堆栈和框架。但那太疯狂了。

你可以用语法来解释它们。基本上,C中的所有控制流语句都有花括号,你可以通过它们相对于括号的位置来区分条件和条件代码。这可能更疯狂。

或者您也可以解释循环、if语句、例程、子例程以及可能的协同例程。

单子可以取代相当多的编程技术。支持它们的语言中有一种特定的语法,以及一些关于它们的理论。

它们也是函数式程序员使用命令式代码而不实际承认它的一种方式,但这并不是他们唯一的用途。

这就是你要找的视频。

在C#中演示组合和对齐类型的问题是什么,然后在C#中正确实现它们。最后,他展示了相同的C#代码在F#中的外观,最后在Haskell中。

解释“什么是monad”有点像说“什么是数字”。我们一直在使用数字。但是假设你遇到了一个对数字一无所知的人。你将如何解释数字是什么?你将如何开始描述为什么它可能有用?

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

本质上,您正在编写执行步骤并将它们与“bind函数”链接在一起。(在Haskell中,它被命名为>>=。)您可以自己编写对bind运算符的调用,或者您可以使用语法糖来让编译器为您插入这些函数调用。但无论哪种方式,每个步骤都由对这个bind函数的调用分隔。

所以bind函数就像一个分号;它将流程中的步骤分开。bind函数的工作是从上一步获取输出,并将其提供给下一步。

听起来不太难,对吧?但是有不止一个种单子。为什么?怎么做?

好吧,bind函数可以只是从一步中获取结果,并将其提供给下一步。但如果这就是monad所做的“全部”……那实际上并不是很有用。理解这一点很重要:每个有用 monad都做了其他此外的事情,而不仅仅是一个monad。每个有用 monad都有一种“特殊的力量”,这使得它独一无二。

(一个具有什么都没有特殊功能的monad被称为“身份monad”。更像是身份函数,这听起来像是一件毫无意义的事情,但事实证明并非如此……但那是另一个故事。)

基本上,每个monad都有自己的bind函数实现。你可以编写一个bind函数,以便它在执行步骤之间执行一些事情。例如:

  • 如果每一步都返回成功/失败指示器,则只有前一步成功时,您才能让bind执行下一步。这样,失败的步骤会“自动”中止整个序列,而无需您进行任何条件测试。(Failure Monad。)

  • 扩展这个想法,您可以实现“异常”。(Error MonadException Monad。)因为您自己定义它们,而不是将其作为语言特性,所以您可以定义它们的工作方式。(例如,也许您想忽略前两个异常,只有在抛出第三个异常时才中止。)

  • 你可以让每一步返回多个结果,并让bind函数循环它们,为您将每个结果都提供给下一步。这样,在处理多个结果时,你不必一直写循环。bind函数“自动”为你完成所有这些。(List Monad。)

  • 除了将“结果”从一个步骤传递到另一个步骤之外,您还可以让bind函数传递额外的数据。这些数据现在不会显示在您的源代码中,但您仍然可以从任何地方访问它,而无需手动将其传递给每个函数。

  • 您可以这样做,以便可以替换“额外数据”。这允许您模拟破坏性更新,而无需实际进行破坏性更新。(State Monad及其表亲Writer Monad。)

  • 因为你只是在模拟破坏性更新,所以你可以做一些在真正的破坏性更新中不可能做的事情。例如,你可以撤消上次更新,或者恢复到旧版本

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

  • 您可以将“延续”实现为monad。这允许您打破人们的想法!

所有这些以及更多的都可以用monad实现。当然,所有这些也完全可能没有 monad。它只是使用monad的更容易

我也在尝试理解monad。这是我的版本:

单子是关于重复事物的抽象。首先,monad本身是一个类型化接口(就像一个抽象的泛型类),它有两个函数:定义了签名的绑定和返回。然后,我们可以基于抽象monad创建具体的monad,当然还有bind和返回的特定实现。此外,bind和返回必须满足一些不变量,才能组合/链接具体monad。

当我们有接口、类型、类和其他工具来创建抽象时,为什么要创建monad概念?因为monad提供了更多:它们以一种无需任何样板即可组合数据的方式强制重新思考问题。

单体似乎是确保在单体上定义的所有操作和支持的类型将始终返回单体内支持的类型的东西。例如,任何数字+任何数字=一个数字,没有错误。

而除法接受两个小数,并返回一个小数,它将除以零定义为haskell中的Infinity(恰好是一个小数)…

无论如何,看起来单子只是一种确保你的操作链以可预测的方式运行的方法,一个声称是Num->Num的函数,由另一个用x调用的Num->Num函数组成,没有说,发射导弹。

另一方面,如果我们有一个发射导弹的函数,我们可以将它与其他发射导弹的函数组合在一起,因为我们的意图很明确-我们想要发射导弹-但它不会出于某种奇怪的原因尝试打印“Hello World”。

在Haskell中,main的类型是IO(),或者IO [()], 这个函数很奇怪,我不会讨论它,但我认为会发生这样的事情:

如果我有main,我希望它执行一系列操作,我运行程序的原因是产生一个效果——通常是通过IO。因此,我可以在main中将IO操作链接在一起,以便——做IO,没有别的。

如果我试图做一些不“返回IO”的事情,程序会抱怨链条不流动,或者基本上是“这与我们试图做的事情有什么关系——IO动作”,它似乎迫使程序员保持他们的思路,而不是偏离并考虑发射导弹,同时创建排序算法——这不会流动。

基本上,Monad似乎是给编译器的提示,“嘿,你知道这个函数在这里返回一个数字,它实际上并不总是有效,它有时可以产生一个数字,有时什么都没有,请记住这一点”。知道了这一点,如果你试图断言一个monadic动作,monadic动作可能会充当编译时异常,说“嘿,这实际上不是一个数字,这可以是一个数字,但你不能假设这一点,做点什么来确保流程是可接受的。”这在一定程度上防止了不可预测的程序行为。

看起来monad不是关于纯洁,也不是关于控制,而是关于维护一个类别的身份,在这个类别上所有的行为都是可预测和定义的,或者不编译的。当你被期望做某事时,你不能什么都不做,如果你被期望什么都不做,你就不能做某事(可见)。

我能想到的Monad的最大原因是——去看看过程/OOP代码,你会注意到你不知道程序从哪里开始,也不知道程序从哪里结束,你所看到的只是大量的跳跃和大量的数学、魔法和导弹。你将无法维护它,如果你可以,你将花费相当多的时间在整个程序周围思考,然后才能理解它的任何部分,因为在这种情况下的模块化是基于相互依赖的代码“部分”,其中代码被优化为尽可能相关,以保证效率/相互关系。单子是非常具体的,并且定义良好,并确保程序流可以分析,并隔离难以分析的部分-因为它们本身就是单子。单子似乎是一个“可理解的单元,在完全理解后是可预测的”——如果你理解“也许”单子,除了“也许”之外,它不可能做任何事情,这看起来微不足道,但在大多数非单子代码中,一个简单的函数“helloworld”可以发射导弹,什么都不做,或者摧毁宇宙,甚至扭曲时间——我们不知道也不能保证它是什么。单子保证它是什么。这是非常强大的。

“现实世界”中的所有事物似乎都是单子,因为它受到明确的可观察定律的约束,以防止混淆。这并不意味着我们必须模仿这个对象的所有操作来创建类,相反,我们可以简单地说“正方形是正方形”,除了正方形什么都没有,甚至不是矩形或圆形,“正方形的面积是它现有维度之一的长度乘以它自己。无论你有什么正方形,如果它是2D空间中的正方形,它的面积绝对不能是任何东西,只能是它长度的平方,证明几乎是微不足道的。这是非常强大的,因为我们不需要做出断言来确保我们的世界是这样的,我们只是使用现实的含义来防止我们的程序偏离轨道。

我几乎肯定是错的,但我认为这可以帮助那里的某人,所以希望它能帮助某人。

数学思维

简而言之:用于组合计算的代数结构。

return data:创建一个在monad世界中简单生成数据的计算。

(return data) >>= (return func):第二个参数接受第一个参数作为数据生成器并创建一个连接它们的新计算。

你可以认为(>>=)返回本身不会做任何计算。他们只是简单地组合和创建计算。

当且仅当主要触发它时,任何monad计算都将被计算。

在Coursera《响应式编程原理》培训中,Erik梅尔将其描述为:

"Monads are return types that guide you through the happy path." -Erik Meijer

在Scala的上下文中,您会发现以下是最简单的定义。基本上,platMap(或bind)是“关联”的,并且存在一个标识。

trait M[+A] {def flatMap[B](f: A => M[B]): M[B] // AKA bind
// Pseudo Meta Codedef isValidMonad: Boolean = {// for every parameter the following holdsdef isAssociativeOn[X, Y, Z](x: M[X], f: X => M[Y], g: Y => M[Z]): Boolean =x.flatMap(f).flatMap(g) == x.flatMap(f(_).flatMap(g))
// for every parameter X and x, there exists an id// such that the following holdsdef isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =x.flatMap(id) == x}}

例如。

// These could be any functionsval f: Int => Option[String] = number => if (number == 7) Some("hello") else Noneval g: String => Option[Double] = string => Some(3.14)
// Observe these are identical. Since Option is a Monad// they will always be identical no matter what the functions arescala> Some(7).flatMap(f).flatMap(g)res211: Option[Double] = Some(3.14)
scala> Some(7).flatMap(f(_).flatMap(g))res212: Option[Double] = Some(3.14)

// As Option is a Monad, there exists an identity:val id: Int => Option[Int] = x => Some(x)
// Observe these are identicalscala> Some(7).flatMap(id)res213: Option[Int] = Some(7)
scala> Some(7)res214: Some[Int] = Some(7)

严格来说,函数式编程中的Monad的定义与范畴理论中的单子的定义不同,后者是按mapflatten轮流定义的。尽管它们在某些映射下是等价的。这个演示非常好:http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category

我仍然是新的单子,但我想我会分享一个链接,我发现感觉真的很好阅读(与图片!!):(无关联)

基本上,我从这篇文章中得到的温暖而模糊的概念是这样一个概念,即monad基本上是允许不同函数以可组合的方式工作的适配器,即能够将多个函数串起来并混合和匹配它们,而不必担心返回类型不一致等。因此,当我们试图制作这些适配器时,BIND函数负责保持苹果和苹果,橙子和橙子。LIFT函数负责接收“较低级别”的函数并“升级”它们以与BIND函数一起使用并且也可组合。

我希望我是对的,更重要的是,希望这篇文章对monads有一个有效的看法。如果没有别的,这篇文章有助于激发我对学习更多monads的兴趣。

在几年前回答了这个问题之后,我相信我可以改进和简化这个答案。

monad是一种函数组合技术,它使用组合函数bind将某些输入场景的处理外部化,以在组合期间预处理输入。

在正常组合中,函数compose (>>)用于将组合函数按顺序应用于其前身的结果。重要的是,正在组合的函数需要处理其输入的所有场景。

(x -> y) >> (y -> z)

这种设计可以通过重构输入来改进,以便更容易地询问相关状态。因此,如果y包含有效性的概念,值可以变成Mb,而不是简单的y,例如,(is_OK, b)

例如,当输入只可能是一个数字时,您可以将类型重组为bool,指示元组中存在有效数字和数字,而不是返回一个可以尽职尽责地包含数字或不包含数字的字符串,例如bool * float。组合函数现在不再需要解析输入字符串来确定数字是否存在,而只需检查元组的bool部分。

(Ma -> Mb) >> (Mb -> Mc)

这里,再一次,组合自然发生在compose中,因此每个函数必须单独处理其输入的所有场景,尽管这样做现在容易得多。

然而,如果我们可以在处理场景是常规的时候将询问的工作外部化呢?例如,如果我们的程序在输入不好时什么也不做,就像is_OKfalse时一样。如果这样做了,那么组合函数就不需要自己处理这种场景了,从而大大简化了它们的代码并实现了另一个级别的重用。

为了实现这种外部化,我们可以使用函数bind (>>=)来执行composition而不是compose。因此,不是简单地将值从一个函数的输出转移到另一个函数的输入Bind将检查MaM部分,并决定是否以及如何将组合函数应用于a。当然,函数bind将专门为我们的特定M定义,以便能够检查其结构并执行我们想要的任何类型的应用程序。尽管如此,a可以是任何东西,因为bind只是在确定应用必要时将a未经检查传递给组合函数。此外,组合函数本身也不再需要处理输入结构的M部分,从而简化了它们。

(a -> Mb) >>= (b -> Mc)或更简洁Mb >>= (b -> Mc)

简而言之,一旦输入被设计成充分暴露它们,monad就会外部化并从而提供围绕某些输入场景处理的标准行为。这种设计是一个shell and content模型,其中shell包含与组合函数的应用程序相关的数据,并被bind函数询问并保持仅对bind函数可用。

因此,monad是三件事:

  1. 用于保存monad相关信息的M shell,
  2. 实现了一个bind函数,以利用此shell信息将组合函数应用于它在shell中找到的内容值,并且
  3. 表单a -> Mb的可组合函数,产生包含单一管理数据的结果。

一般来说,函数的输入比它的输出限制得多,输出可能包括错误条件等;因此,Mb结果结构通常非常有用。例如,当除数为0时,除法运算符不返回数字。

此外,monad可能包括包装函数,这些包装函数通过在应用程序后包装它们的结果,将值a包装成一元类型Ma,将通用函数a -> b包装成一元函数a -> Mb。当然,像bind一样,这样的包装函数是特定于M的。一个例子:

let return a = [a]let lift f a = return (f a)

bind函数的设计假设不可变的数据结构和纯函数,其他事情变得复杂并且无法保证。因此,有一元定律:

鉴于…

M_return = (a -> Ma)f = (a -> Mb)g = (b -> Mc)

那…

Left Identity  : (return a) >>= f === f aRight Identity : Ma >>= return    === MaAssociative    : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)

Associativity表示无论何时应用bindbind都保留求值顺序。也就是说,在上述Associativity的定义中,对fg的括号中的binding的强制早期求值只会导致一个期望Ma以完成bind的函数。因此,必须在将其值应用于f并将结果应用于g之前确定Ma的求值。

世界需要的是另一个monad博客文章,但我认为这对识别野外现有的monad很有用。

谢尔宾斯基三角形

上面是一个叫做谢尔宾斯基三角形的分形,这是我能记得画的唯一一个分形。分形是像上面三角形一样的自相似结构,其中部分与整体相似(在这种情况下正好是父三角形规模的一半)。

单子是分形。给定一个一元数据结构,它的值可以组合成数据结构的另一个值。这就是为什么它对编程很有用,这就是为什么它在许多情况下会发生。

一个非常简单的答案是:

单子是一个抽象,它提供了一个接口来封装值、计算新的封装值和解包封装值。

他们在实践中方便的是它们提供了一个统一的接口,用于创建建模状态而不是有状态的数据类型

重要的是要理解Monad是抽象,即用于处理某种数据结构的抽象接口。然后该接口用于构建具有一元行为的数据类型。

你可以在Ruby中的单子,第1部分:介绍中找到一个非常好和实用的介绍。

这个答案从一个激励性的例子开始,通过例子工作,派生一个monad的例子,并正式定义“monad”。

在伪代码中考虑这三个函数:

f(<x, messages>) := <x, messages "called f. ">g(<x, messages>) := <x, messages "called g. ">wrap(x)          := <x, "">

f接受形式为<x, messages>的有序对并返回一个有序对。它保持第一个项目不变,并将"called f. "附加到第二个项目。与g相同。

您可以组合这些函数并获取您的原始值,以及一个显示函数调用顺序的字符串:

  f(g(wrap(x)))= f(g(<x, "">))= f(<x, "called g. ">)= <x, "called g. called f. ">

您不喜欢这样一个事实,即fg负责将它们自己的日志消息附加到先前的日志信息中。(为了便于讨论,请想象一下,fg必须在对的第二项上执行复杂的逻辑,而不是附加字符串。在两个或更多不同的函数中重复复杂的逻辑会很痛苦。)

您更喜欢编写更简单的函数:

f(x)    := <x, "called f. ">g(x)    := <x, "called g. ">wrap(x) := <x, "">

但是看看当你组合它们时会发生什么:

  f(g(wrap(x)))= f(g(<x, "">))= f(<<x, "">, "called g. ">)= <<<x, "">, "called g. ">, "called f. ">

问题是通过一对到函数中并不能给你想要的东西。但是如果你可以一对到函数中会怎么样:

  feed(f, feed(g, wrap(x)))= feed(f, feed(g, <x, "">))= feed(f, <x, "called g. ">)= <x, "called g. called f. ">

feed(f, m)读作“将m送入f”。要将m0一对<x, messages>送入函数f,就是将m1x送入f,从f中获取<y, message>,然后返回<y, messages message>

feed(f, <x, messages>) := let <y, message> = f(x)in  <y, messages message>

注意当你用函数做三件事时会发生什么:

首先:如果你包装一个值,然后将结果对到一个函数中:

  feed(f, wrap(x))= feed(f, <x, "">)= let <y, message> = f(x)in  <y, "" message>= let <y, message> = <x, "called f. ">in  <y, "" message>= <x, "" "called f. ">= <x, "called f. ">= f(x)

这与通过函数中的值相同。

第二:如果你将一对输入wrap

  feed(wrap, <x, messages>)= let <y, message> = wrap(x)in  <y, messages message>= let <y, message> = <x, "">in  <y, messages message>= <x, messages "">= <x, messages>

这并没有改变货币对。

第三:如果您定义了一个接受x并将g(x)馈入f的函数:

h(x) := feed(f, g(x))

并在其中添加一对:

  feed(h, <x, messages>)= let <y, message> = h(x)in  <y, messages message>= let <y, message> = feed(f, g(x))in  <y, messages message>= let <y, message> = feed(f, <x, "called g. ">)in  <y, messages message>= let <y, message> = let <z, msg> = f(x)in  <z, "called g. " msg>in <y, messages message>= let <y, message> = let <z, msg> = <x, "called f. ">in  <z, "called g. " msg>in <y, messages message>= let <y, message> = <x, "called g. " "called f. ">in <y, messages message>= <x, messages "called g. " "called f. ">= feed(f, <x, messages "called g. ">)= feed(f, feed(g, <x, messages>))

这与将该对送入g并将结果对送入f相同。

你有大部分的monad。现在你只需要知道你的程序中的数据类型。

<x, "called f. ">是什么类型的值?嗯,这取决于x是什么类型的值。如果xt类型,那么你的对是“t和字符串对”类型的值。称该类型为M t

M是一个类型构造函数:M本身并不引用一个类型,但是M _引用了一个类型,一旦你用一个类型填空。M int是一对int和一个字符串。M string是一对字符串和一个字符串。等等。

恭喜你,你创造了一个单子!

形式上,你的monad是元组<M, feed, wrap>

monad是一个元组<M, feed, wrap>,其中:

  • M是一个类型构造函数。
  • feed接受一个(接受t并返回M u的函数)和一个M t并返回M u
  • wrap接受v并返回M v

tuv是可能相同也可能不相同的任何三种类型。monad满足你为特定monad证明的三个属性:

  • 喂养t包装到函数中与通过t未包装到函数中相同。

    格式:feed(f, wrap(x)) = f(x)

  • M t输入wrapM t没有任何影响。

    格式:feed(wrap, m) = m

  • M t(称之为m)送入一个函数

    • t转换为g
    • g获取M u(称之为n
    • n输入f

    • m导入g
    • g得到n
    • n导入f

    格式:feed(h, m) = feed(f, feed(g, m))其中h(x) := feed(f, g(x))

通常,feed称为bind(Haskell中的AKA>>=),wrap称为return

在实践中,monad是函数组合运算符的自定义实现,它负责副作用和不兼容的输入和返回值(用于链接)。

本质上实际上,monad允许回调嵌套
(具有相互递归线程状态(请原谅连字符))
(以可组合(或可分解)的方式)
(使用类型安全(有时(取决于语言)))
)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))

例如,这是不是 a monad:

//JavaScript is 'Practical'var getAllThree =bind(getFirst, function(first){return bind(getSecond,function(second){return bind(getThird, function(third){var fancyResult = // And now make do fancy// with first, second,// and thirdreturn RETURN(fancyResult);});});});

但是monad启用了这样的代码。
monad实际上是以下类型的集合:
{bind,RETURN,maybe others I don't know...}.
基本上不必要,实际上不切实际。

现在我可以使用它:

var fancyResultReferenceOutsideOfMonad =getAllThree(someKindOfInputAcceptableToOurGetFunctionsButProbablyAString);
//Ignore this please, throwing away types, yay JavaScript://  RETURN = K//  bind = \getterFn,cb ->//    \in -> let(result,newState) = getterFn(in) in cb(result)(newState)

或者打破它:

var getFirstTwo =bind(getFirst, function(first){return bind(getSecond,function(second){var fancyResult2 = // And now make do fancy// with first and secondreturn RETURN(fancyResult2);});}), getAllThree =bind(getFirstTwo, function(fancyResult2){return bind(getThird,    function(third){var fancyResult3 = // And now make do fancy// with fancyResult2,// and thirdreturn RETURN(fancyResult3);});});

或者忽略某些结果:

var getFirstTwo =bind(getFirst, function(first){return bind(getSecond,function(second){var fancyResult2 = // And now make do fancy// with first and secondreturn RETURN(fancyResult2);});}), getAllThree =bind(getFirstTwo, function(____dontCare____NotGonnaUse____){return bind(getThird,    function(three){var fancyResult3 = // And now make do fancy// with `three` only!return RETURN(fancyResult3);});});

或者简化一个琐碎的案例:

var getFirstTwo =bind(getFirst, function(first){return bind(getSecond,function(second){var fancyResult2 = // And now make do fancy// with first and secondreturn RETURN(fancyResult2);});}), getAllThree =bind(getFirstTwo, function(_){return bind(getThird,    function(three){return RETURN(three);});});

到(使用“正确的身份”):

var getFirstTwo =bind(getFirst, function(first){return bind(getSecond,function(second){var fancyResult2 = // And now make do fancy// with first and secondreturn RETURN(fancyResult2);});}), getAllThree =bind(getFirstTwo, function(_){return getThird;});

或者把它们粘在一起:

var getAllThree =bind(getFirst, function(first_dontCareNow){return bind(getSecond,function(second_dontCareNow){return getThird;});});

这些能力的实用性并没有真正出现,
或者变得清晰,直到你试图解决真正混乱的问题。像解析,或模块/ajax/资源加载。

你能想象成千上万行indexOf/subString逻辑吗?
如果频繁的解析步骤包含在小函数中怎么办?
charsspacesupperCharsdigits之类的函数?
如果这些函数在回调中给你结果,
无需与正则表达式组混淆,arguments.slice?
如果它们的组成/分解被很好地理解呢?
这样你就可以自下而上地构建大型解析器了?

因此,管理嵌套回调范围非常实用的能力,
特别是在使用一元解析器组合器库时。
(根据我的经验)

别挂电话
-分类理论
-可能元
-MONAD LAWS
-HASKell
-!!!!!

补充说明

这很简单,当用C#/Java术语解释时:

  1. monad是一个接受参数并返回特殊类型的函数。

  2. 这个monad返回的特殊类型是,也称为monad。(monad是#1和#2的组合)

  3. 有一些语法糖可以使调用此函数和类型转换更容易。

示例

monad可以让函数式程序员的生活更轻松。典型的例子:Maybe monad接受两个参数,一个值和一个函数。如果传递的值是null,它返回null。否则它计算函数。如果我们需要一个特殊的返回类型,我们也会称这个返回类型为Maybe。一个非常粗糙的实现如下所示:

object Maybe(object value, Func<object,object> function){if(value==null)return null;
return function(value);}

这在C#中非常无用,因为这种语言缺乏使monad有用所需的语法糖。但是monads允许您用函数式编程语言编写更简洁的代码。

程序员经常在链中调用monad,如下所示:

var x = Maybe(x, x2 => Maybe(y, y2 => Add(x2, y2)));

在此示例中,只有当xy都非null时才会调用Add方法,否则将返回null

答案

回答最初的问题:monad是一个函数和一个类型。就像一个特殊的interface的实现。

让下面的“{| a |m}”代表一些一元数据。宣传a的数据类型:

        (I got an a!)/{| a |m}

函数f知道如何创建一个monad,如果它有一个a

       (Hi f! What should I be?)/(You?. Oh, you'll be /that data there.)  //                 /  (I got a b.)|    --------------      ||  /                     |f a                      ||--later->       {| b |m}

这里我们看到函数f试图评估一个monad,但被斥责。

(Hmm, how do I get that a?)o       (Get lost buddy.o         Wrong type.)o       /f {| a |m}

函数f找到了一种使用>>=提取a的方法。

        (Muaahaha. How youlike me now!?)(Better.)      \|     (Give me that a.)(Fine, well ok.)    |\          |{| a |m}   >>=   f

f不知道,monad和>>=是勾结的。

            (Yah got an a for me?)(Yeah, but hey    |listen. I got    |something to     |tell you first   |...)   \        /|      /{| a |m}   >>=   f

但是他们实际上在谈论什么呢?嗯,这取决于单子。仅仅抽象地谈论用处有限;你必须有一些特定单子的经验来充实理解。

例如,数据类型可能

 data Maybe a = Nothing | Just a

有一个monad实例,其行为如下…

其中,如果情况为Just a

            (Yah what is it?)(... hm? Oh,      |forget about it.  |Hey a, yr up.)    |\     |(Evaluation  \    |time already? \   |Hows my hair?) |  ||       /   ||  (It's    ||  fine.)  /|   /     /{| a |m}   >>=   f

但对于Nothing的情况

        (Yah what is it?)(... There      |is no a. )      ||        (No a?)(No a.)         ||        (Ok, I'll deal|         with this.)\            |\      (Hey f, get lost.)\          |   ( Where's my a?\         |     I evaluate a)\    (Not any more  |\    you don't.    ||   We're returning|   Nothing.)   /|      |       /|      |      /|      |     /{| a |m}   >>=   f      (I got a b.)|  (This is   \|   such a     \|   sham.) o o  \|               o||--later-> {| b |m}

因此,如果计算实际上包含了它所通告的a,也许monad会让计算继续,但如果没有,则中止计算。然而,结果仍然是一段一元数据,尽管不是f的输出。出于这个原因,也许monad用于表示失败的上下文。

不同的monad行为不同。列表是具有一元实例的其他类型的数据。它们的行为如下:

(Ok, here's your a. Well, itsa bunch of them, actually.)||    (Thanks, no problem. Ok|     f, here you go, an a.)|       ||       |        (Thank's. See|       |         you later.)|  (Whoa. Hold up f,      ||   I got another         ||   a for you.)           ||       |      (What? No, sorry.|       |       Can't do it. I|       |       have my hands full|       |       with all these "b"|       |       I just made.)|  (I'll hold those,      ||   you take this, and   /|   come back for more  /|   when you're done   /|   and we'll do it   /|   again.)          /\      |  ( Uhhh. All right.)\     |       /\    \      /{| a |m}   >>=  f

在这种情况下,函数知道如何从它的输入中生成一个列表,但不知道如何处理额外的输入和额外的列表。绑定>>=通过组合多个输出来帮助f。我包括这个例子是为了表明,虽然>>=负责提取a,但它也可以访问f的最终绑定输出。事实上,除非它知道最终输出具有相同类型的上下文,否则它永远不会提取任何a

还有其他的monad用于表示不同的上下文。这里有一些更多的特征。IO monad实际上没有a,但它认识一个人,会为你得到aState st monad有一个秘密的st,它会传递给桌子下面的f,即使f只是来要求aReader r monad类似于State st,尽管它只让fa1。

所有这一切的重点是,任何类型的数据被声明为Monad,都是在围绕从monad中提取一个值声明某种上下文。所有这一切的巨大收益?好吧,用某种上下文进行计算很容易。然而,当将多个上下文负载的计算串在一起时,它会变得混乱。monad操作负责解析上下文的交互,这样程序员就不必这样做了。

请注意,使用>>=通过从f那里拿走一些自主权来缓解混乱。也就是说,在上述Nothing的情况下,f不再有权决定在Nothing的情况下该做什么;它被编码在>>=中。这是权衡。如果f有必要决定在Nothing的情况下该做什么,那么f应该是Maybe af0的函数。在这种情况下,f1是单子是无关紧要的。

但是请注意,有时数据类型不会导出它的构造函数(看着你的IO),如果我们想使用广告值,我们别无选择,只能使用它的一元接口。

另一个解释monad的尝试,仅使用Python列表和map函数。我完全接受这不是一个完整的解释,但我希望它能抓住核心概念。

我从Monads上的funfunfunction视频和学习你Haskell章节“为了更多的几个单子”中得到了这个基础。我高度建议观看funfunaction视频。

最简单的是,Monad是具有mapflatMap函数(在Haskell中为bind)的对象。有一些额外的必需属性,但这些是核心。

flatMap平坦map的输出,对于列表,这只是连接列表的值,例如。

concat([[1], [4], [9]]) = [1, 4, 9]

所以在Python中,我们基本上可以用这两个函数实现Monad:

def flatMap(func, lst):return concat(map(func, lst))
def concat(lst):return sum(lst, [])

func是任何接受值并返回列表的函数,例如。

lambda x: [x*x]

补充说明

为了清楚起见,我通过简单函数在Python中创建了concat函数,它对列表进行了求和,即[] + [1] + [4] + [9] = [1, 4, 9](Haskell有一个原生的concat方法)。

我假设你知道map函数是什么,例如:

>>> list(map(lambda x: [x*x], [1,2,3]))[[1], [4], [9]]

扁平化是Monad的关键概念,对于每个Monad对象,这种扁平化允许您获取Monad中包含的值。

现在我们可以调用:

>>> flatMap(lambda x: [x*x], [1,2,3])[1, 4, 9]

这个lambda获取一个值x并将其放入一个列表中。monad适用于从值到monad类型的任何函数,因此在本例中是一个列表。

这是你的单子定义

我认为为什么它们有用的问题已经在其他问题中得到了回答。

更多解释

其他不是列表的示例是JavaScript Promises,它具有then方法和JavaScript Streams,它具有flatMap方法。

因此,Promises和Streams使用了一个略有不同的函数,它将Stream或Promise展平并从内部返回值。

haskell列表单子有以下定义:

instance Monad [] wherereturn x = [x]xs >>= f = concat (map f xs)fail _ = []

即有三个函数return(不要与大多数其他语言中的back混淆)、>>=flatMap)和fail

希望你能看到它们之间的相似之处:

xs >>= f = concat (map f xs)

和:

def flatMap(f, xs):return concat(map(f, xs))

我将尝试在Haskell的上下文中解释Monad

在函数式编程中,函数组合很重要。它允许我们的程序由小的、易于阅读的函数组成。

假设我们有两个函数:g :: Int -> Stringf :: String -> Bool

我们可以做(f . g) x,这与f (g x)相同,其中xInt值。

将一个函数的结果组合/应用到另一个函数时,类型匹配很重要。在上述情况下,g返回的结果类型必须与f接受的类型相同。

但是有时值在上下文中,这使得对类型进行排队有点不那么容易。(在上下文中具有值非常有用。例如,Maybe Int类型表示可能不存在的Int值,IO String类型表示由于执行一些副作用而存在的String值。)

假设我们现在有g1 :: Int -> Maybe Stringf1 :: String -> Maybe Boolg1f1分别与gf非常相似。

我们不能做(f1 . g1) xf1 (g1 x),其中xInt的值。g1返回的结果类型不是f1所期望的。

我们可以用.运算符组合fg,但现在我们不能用.组合f1g1。问题是我们不能直接将上下文中的值传递给期望不在上下文中的值的函数。

如果我们引入一个运算符来组合g1f1,这样我们就可以编写(f1 OPERATOR g1) x,这不是很好吗?g1在上下文中返回一个值。该值将脱离上下文并应用于f1。是的,我们有这样一个运算符。它是<=<

我们还有>>=运算符,它为我们做了完全相同的事情,尽管语法略有不同。

我们写:g1 x >>= f1g1 xMaybe Int值。>>=运算符帮助将Int值从“也许不存在”上下文中取出,并将其应用于f1f1的结果,即Maybe Bool,将是整个>>=操作的结果。

最后,为什么Monad有用?因为Monad是定义>>=运算符的类型类,与定义==/=运算符的Eq类型类非常相似。

总而言之,Monad类型类定义了>>=运算符,它允许我们将上下文中的值(我们称之为一元值)传递给不期望上下文中值的函数。上下文将得到照顾。

如果这里有一件事要记住,那就是#0允许在上下文中包含值的函数组合

tl; dr

{-# LANGUAGE InstanceSigs #-}
newtype Id t = Id t
instance Monad Id wherereturn :: t -> Id treturn = Id
(=<<) :: (a -> Id b) -> Id a -> Id bf =<< (Id x) = f x

序幕

函数的应用程序运算符$

forall a b. a -> b

是规范定义的

($) :: (a -> b) -> a -> bf $ x = f x
infixr 0 $

就Haskell原语函数应用程序f xinfixl 10)而言。

组合物.根据$定义为

(.) :: (b -> c) -> (a -> b) -> (a -> c)f . g = \ x -> f $ g x
infixr 9 .

并满足等价forall f g h.

     f . id  =  f            :: c -> d   Right identityid . g  =  g            :: b -> c   Left identity(f . g) . h  =  f . (g . h)  :: a -> d   Associativity

.是关联的,id是它的左右标识。

Kleisli三人组

在编程中,monad是具有monad类型类实例的仿函数类型构造函数。有几个等价的定义和实现变体,每个变体对monad抽象的直觉略有不同。

仿函数是类型* -> *的类型构造函数f,具有仿函数类型类的实例。

{-# LANGUAGE KindSignatures #-}
class Functor (f :: * -> *) wheremap :: (a -> b) -> (f a -> f b)

除了遵循静态强制类型协议外,仿函数类型类的实例必须遵守代数仿函数定律forall f g.

       map id  =  id           :: f t -> f t   Identitymap f . map g  =  map (f . g)  :: f a -> f c   Composition / short cut fusion

函数计算的类型

forall f t. Functor f => f t

计算c r包含在背景c中的结果r中。

一元一元函数或Kleisli箭头具有以下类型

forall m a b. Functor m => a -> m b

Kleisi箭头是接受一个参数a并返回一元计算m b的函数。

单子是根据Kleisli三重forall m. Functor m =>规范定义的

(m, return, (=<<))

实现为类型类

class Functor m => Monad m wherereturn :: t -> m t(=<<)  :: (a -> m b) -> m a -> m b
infixr 1 =<<

Kleisli身份return是一个Kleisli箭头,它将值t提升到一元上下文m扩展Kleisli应用=<<将Kleisli箭头a -> m b应用于计算m a的结果。

Kleisli构图<=<在扩展方面定义为

(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)f <=< g = \ x -> f =<< g x
infixr 1 <=<

<=<组成两个Kleisli箭头,将左箭头应用于右箭头应用的结果。

monad类型类的实例必须遵守单子定律,这是Kleisli组合中最优雅的陈述:forall f g h.

   f <=< return  =  f                :: c -> m d   Right identityreturn <=< g  =  g                :: b -> m c   Left identity(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d   Associativity

<=<是关联的,return是它的左右标识。

身份

的身份类型

type Id t = t

是类型上的单位函数

Id :: * -> *

解释为仿函数,

   return :: t -> Id t=      id :: t ->    t
(=<<) :: (a -> Id b) -> Id a -> Id b=     ($) :: (a ->    b) ->    a ->    b
(<=<) :: (b -> Id c) -> (a -> Id b) -> (a -> Id c)=     (.) :: (b ->    c) -> (a ->    b) -> (a ->    c)

在规范Haskell中,标识monad被定义为

newtype Id t = Id t
instance Functor Id wheremap :: (a -> b) -> Id a -> Id bmap f (Id x) = Id (f x)
instance Monad Id wherereturn :: t -> Id treturn = Id
(=<<) :: (a -> Id b) -> Id a -> Id bf =<< (Id x) = f x

选项

选项类型

data Maybe t = Nothing | Just t

编码不一定产生结果t的计算Maybe t,可能“失败”的计算。定义了选项monad

instance Functor Maybe wheremap :: (a -> b) -> (Maybe a -> Maybe b)map f (Just x) = Just (f x)map _ Nothing  = Nothing
instance Monad Maybe wherereturn :: t -> Maybe treturn = Just
(=<<) :: (a -> Maybe b) -> Maybe a -> Maybe bf =<< (Just x) = f x_ =<< Nothing  = Nothing

只有当Maybe a产生结果时,a -> Maybe b才应用于结果。

newtype Nat = Nat Int

自然数可以被编码为大于或等于零的整数。

toNat :: Int -> Maybe NattoNat i | i >= 0    = Just (Nat i)| otherwise = Nothing

自然数在减法下不闭合。

(-?) :: Nat -> Nat -> Maybe Nat(Nat n) -? (Nat m) = toNat (n - m)
infixl 6 -?

选项monad涵盖了异常处理的基本形式。

(-? 20) <=< toNat :: Int -> Maybe Nat

列表

列表monad,在列表类型上

data [] t = [] | t : [t]
infixr 5 :

和它的可加的二元运算"append"

(++) :: [t] -> [t] -> [t](x : xs) ++ ys = x : xs ++ ys[]       ++ ys = ys
infixr 5 ++

编码非线性计算[t]产生结果t的自然量0, 1, ...

instance Functor [] wheremap :: (a -> b) -> ([a] -> [b])map f (x : xs) = f x : map f xsmap _ []       = []
instance Monad [] wherereturn :: t -> [t]return = (: [])
(=<<) :: (a -> [b]) -> [a] -> [b]f =<< (x : xs) = f x ++ (f =<< xs)_ =<< []       = []

扩展=<<将从Kleisli箭头a -> [b]的应用程序f x[a]的元素产生的所有列表[b]连接到单个结果列表[b]

设正整数n的适当因子为

divisors :: Integral t => t -> [t]divisors n = filter (`divides` n) [2 .. n - 1]
divides :: Integral t => t -> t -> Bool(`divides` n) = (== 0) . (n `rem`)

然后

forall n.  let { f = f <=< divisors } in f n   =   []

在定义monad类型类时,Haskell标准使用它的翻盖,绑定运算符>>=,而不是扩展名=<<

class Applicative m => Monad m where(>>=) :: forall a b. m a -> (a -> m b) -> m b
(>>) :: forall a b. m a -> m b -> m bm >> k = m >>= \ _ -> k{-# INLINE (>>) #-}
return :: a -> m areturn = pure

为了简单起见,这个解释使用类型类层次结构

class              Functor fclass Functor m => Monad m

在Haskell中,当前的标准层次结构是

class                  Functor fclass Functor p     => Applicative pclass Applicative m => Monad m

因为不仅每个monad都是仿函数,而且每个应用程序都是仿函数,每个monad也是应用程序。

使用list monad,命令式伪代码

for a in (1, ..., 10)for b in (1, ..., 10)p <- a * bif even(p)yield p

大致翻译为确实阻止

do a <- [1 .. 10]b <- [1 .. 10]let p = a * bguard (even p)return p

等价的单子理解

[ p | a <- [1 .. 10], b <- [1 .. 10], let p = a * b, even p ]

和表达

[1 .. 10] >>= (\ a ->[1 .. 10] >>= (\ b ->let p = a * b inguard (even p) >>       -- [ () | even p ] >>return p))

Do表示法和monad理解是嵌套绑定表达式的语法糖。绑定运算符用于一元结果的本地名称绑定。

let x = v in e    =   (\ x -> e)  $  v   =   v  &  (\ x -> e)do { r <- m; c }  =   (\ r -> c) =<< m   =   m >>= (\ r -> c)

在哪里

(&) :: a -> (a -> b) -> b(&) = flip ($)
infixl 0 &

守卫功能已定义

guard :: Additive m => Bool -> m ()guard True  = return ()guard False = fail

其中单元类型或“空元组”

data () = ()

支持选择故障加性单子可以使用类型类抽象

class Monad m => Additive m wherefail  :: m t(<|>) :: m t -> m t -> m t
infixl 3 <|>
instance Additive Maybe wherefail = Nothing
Nothing <|> m = mm       <|> _ = m
instance Additive [] wherefail = [](<|>) = (++)

其中fail<|>形成一个二次单体

     k <|> fail  =  kfail <|> l  =  l(k <|> l) <|> m  =  k <|> (l <|> m)

fail是加性单子的吸收/湮灭零元素

_ =<< fail  =  fail

如果在

guard (even p) >> return p

even p为true,则守卫产生[()],并且根据>>的定义,局部常量函数

\ _ -> return p

被应用于结果()。如果为false,则守卫生成列表monad的fail[]),这不会产生将>>应用于Kleisli箭头的结果,因此跳过此p

国家

臭名昭著的是,monad用于编码有状态计算。

状态处理器是一个函数

forall st t. st -> (t, st)

转换状态st并产生结果tst可以是任何东西。没有,标志,计数,数组,句柄,机器,世界。

状态处理器的类型通常称为

type State st t = st -> (t, st)

状态处理器monad是kinded* -> *仿函数State st。状态处理器monad的Kleisli箭头是函数

forall st a b. a -> (State st) b

在规范Haskell中,定义了状态处理器monad的惰性版本

newtype State st t = State { stateProc :: st -> (t, st) }
instance Functor (State st) wheremap :: (a -> b) -> ((State st) a -> (State st) b)map f (State p) = State $ \ s0 -> let (x, s1) = p s0in  (f x, s1)
instance Monad (State st) wherereturn :: t -> (State st) treturn x = State $ \ s -> (x, s)
(=<<) :: (a -> (State st) b) -> (State st) a -> (State st) bf =<< (State p) = State $ \ s0 -> let (x, s1) = p s0in  stateProc (f x) s1

状态处理器通过提供初始状态来运行:

run :: State st t -> st -> (t, st)run = stateProc
eval :: State st t -> st -> teval = fst . run
exec :: State st t -> st -> stexec = snd . run

状态访问由原语getput提供,抽象方法超过有状态 monad:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st whereget :: m stput :: st -> m ()

m -> st在monadm上声明了状态类型st功能依赖;例如,State t将唯一地确定状态类型为t

instance Stateful (State st) st whereget :: State st stget = State $ \ s -> (s, s)
put :: st -> State st ()put s = State $ \ _ -> ((), s)

单位类型类似于C中的void

modify :: Stateful m st => (st -> st) -> m ()modify f = dos <- getput (f s)
gets :: Stateful m st => (st -> t) -> m tgets f = dos <- getreturn (f s)

gets通常与记录字段访问器一起使用。

变量线程的状态monad等效

let s0 = 34s1 = (+ 1) s0n = (* 12) s1s2 = (+ 7) s1in  (show n, s2)

其中s0 :: Int是同样透明的,但更优雅和实用

(flip run) 34(domodify (+ 1)n <- gets (* 12)modify (+ 7)return (show n))

modify (+ 1)是类型State Int ()的计算,除了它的效果等价于return ()

(flip run) 34(modify (+ 1) >>gets (* 12) >>= (\ n ->modify (+ 7) >>return (show n)))

结合性的单子定律可以写成>>=forall m f g.

(m >>= f) >>= g  =  m >>= (\ x -> f x >>= g)

do {                 do {                   do {r1 <- do {           x <- m;                r0 <- m;r0 <- m;   =      do {            =      r1 <- f r0;f r0                 r1 <- f x;          g r1};                      g r1             }g r1                 }}                    }

与面向表达式的编程(例如Rust)一样,块的最后一条语句代表其产量。绑定运算符有时称为“可编程分号”。

来自结构化命令式编程的迭代控制结构原语以单向方式模拟

for :: Monad m => (a -> m b) -> [a] -> m ()for f = foldr ((>>) . f) (return ())
while :: Monad m => m Bool -> m t -> m ()while c m = dob <- cif b then m >> while c melse return ()
forever :: Monad m => m tforever m = m >> forever m

输入/输出

data World

I/O世界状态处理器monad是纯Haskell和现实世界的调和,是功能外延和命令式操作语义学的调和。与实际严格实现的密切相似:

type IO t = World -> (t, World)

不纯原语促进了交互

getChar         :: IO CharputChar         :: Char -> IO ()readFile        :: FilePath -> IO StringwriteFile       :: FilePath -> String -> IO ()hSetBuffering   :: Handle -> BufferMode -> IO ()hTell           :: Handle -> IO Integer. . .              . . .

使用IO原语的代码的杂质由类型系统永久地协议化。因为纯度很棒,所以在IO中发生的事情留在IO中。

unsafePerformIO :: IO t -> t

或者,至少,应该。

Haskell程序的类型签名

main :: IO ()main = putStrLn "Hello, World!"

扩展到

World -> ((), World)

一个改变世界的函数。

尾声

对象是Haskell类型,态射是Haskell类型之间的函数的类别是“快速和松散”,类别Hask

仿函数T是从类别C到类别D的映射;对于C中的每个对象,D中的一个对象

Tobj :  Obj(C) -> Obj(D)f :: *      -> *

对于C中的每个态射,D中的一个态射

Tmor :  HomC(X, Y) -> HomD(Tobj(X), Tobj(Y))map :: (a -> b)   -> (f a -> f b)

其中XYC中的对象。HomC(X, Y)C中所有态射X -> Y中的同态类。仿函数必须在D中保留态射标识和组合,即C的“结构”。

                    Tmor    Tobj
T(id)  =  id        : T(X) -> T(X)   IdentityT(f) . T(g)  =  T(f . g)  : T(X) -> T(Z)   Composition

类别CKleisli类别由Kleisli三元组给出

<T, eta, _*>

内函子的

T : C -> C

f)、恒等式态射etareturn)和扩展运算符*=<<)。

Hask中的每个Kleisli态射

      f :  X -> T(Y)f :: a -> m b

由扩展操作符

   (_)* :  Hom(X, T(Y)) -> Hom(T(X), T(Y))(=<<) :: (a -> m b)   -> (m a -> m b)

Hask的Kleisli范畴中给定一个态射

     f* :  T(X) -> T(Y)(f =<<) :: m a  -> m b

Kleisli类别.T中的组成以扩展形式给出

 f .T g  =  f* . g       :  X -> T(Z)f <=< g  =  (f =<<) . g  :: a -> m c

并满足范畴公理

       eta .T g  =  g                :  Y -> T(Z)   Left identityreturn <=< g  =  g                :: b -> m c
f .T eta  =  f                :  Z -> T(U)   Right identityf <=< return  =  f                :: c -> m d
(f .T g) .T h  =  f .T (g .T h)    :  X -> T(U)   Associativity(f <=< g) <=< h  =  f <=< (g <=< h)  :: a -> m d

其中,应用等价变换

     eta .T g  =  geta* . g  =  g               By definition of .Teta* . g  =  id . g          forall f.  id . f  =  feta*  =  id              forall f g h.  f . h  =  g . h  ==>  f  =  g
(f .T g) .T h  =  f .T (g .T h)(f* . g)* . h  =  f* . (g* . h)   By definition of .T(f* . g)* . h  =  f* . g* . h     . is associative(f* . g)*  =  f* . g*         forall f g h.  f . h  =  g . h  ==>  f  =  g

在扩展方面是规范给出的

               eta*  =  id                 :  T(X) -> T(X)   Left identity(return =<<)  =  id                 :: m t -> m t
f* . eta  =  f                  :  Z -> T(U)      Right identity(f =<<) . return  =  f                  :: c -> m d
(f* . g)*  =  f* . g*            :  T(X) -> T(Z)   Associativity(((f =<<) . g) =<<)  =  (f =<<) . (g =<<)  :: m a -> m c

单子也可以用Kleisian扩展来定义,但是在称为join的编程中,自然变换mu。单子被定义为mu作为内函子的范畴C上的三元组

     T :  C -> Cf :: * -> *

和两种自然变化

   eta :  Id -> Treturn :: t  -> f t
mu :  T . T   -> Tjoin :: f (f t) -> f t

满足等价

       mu . T(mu)  =  mu . mu               :  T . T . T -> T . T   Associativityjoin . map join  =  join . join           :: f (f (f t)) -> f t
mu . T(eta)  =  mu . eta       =  id  :  T -> T               Identityjoin . map return  =  join . return  =  id  :: f t -> f t

然后定义monad类型类

class Functor m => Monad m wherereturn :: t -> m tjoin   :: m (m t) -> m t

选项monad的规范mu实现:

instance Monad Maybe wherereturn = Just
join (Just m) = mjoin Nothing  = Nothing

concat函数

concat :: [[a]] -> [a]concat (x : xs) = x ++ concat xsconcat []       = []

是列表monad的join

instance Monad [] wherereturn :: t -> [t]return = (: [])
(=<<) :: (a -> [b]) -> ([a] -> [b])(f =<<) = concat . map f

join的实现可以使用等价从扩展形式转换

     mu  =  id*           :  T . T -> Tjoin  =  (id =<<)      :: m (m t) -> m t

mu到扩展形式的反向转换由

     f*  =  mu . T(f)     :  T(X) -> T(Y)(f =<<)  =  join . map f  :: m a -> m b

但是为什么一个如此抽象的理论对编程有任何用处呢?

答案很简单:作为计算机科学家,我们价值抽象!当我们设计软件组件的接口时,我们想要它尽可能少地揭示实现。我们希望能够用许多替代方案来替换实现,许多其他相同“概念”的“实例”。当我们为许多程序库设计通用接口时,我们选择的接口有多种实现更为重要。这是我们高度重视的monad概念的通用性,它是因为范畴理论如此抽象,以至于它的概念对编程如此有用。

因此,我们下面介绍的单子的推广也与范畴理论密切相关,这并不奇怪。但我们强调,我们的目的是非常实用的:它不是“实现范畴理论”,而是找到一种更通用的方法来构建组合子库。数学家已经为我们做了很多工作,这简直是我们的幸运!

作者:John Hughes

如果你要求对如此抽象的东西进行简洁,实用的解释,那么你只能希望得到一个抽象的答案:

a -> b

是表示从ab的计算的一种方式。您可以将计算链接在一起,也就是组成

(b -> c) -> (a -> b) -> (a -> c)

更复杂的计算需要更复杂的类型,例如:

a -> f b

是从abf的计算类型。你也可以组合它们:

(b -> f c) -> (a -> f b) -> (a -> f c)

事实证明,这种模式几乎无处不在,并且与上面的第一个组合具有相同的属性(结合性,右和左同一性)。

人们必须给这个模式一个名字,但是知道第一个组成被正式描述为半足类会有帮助吗?

“单子和括号一样有趣和重要。(奥列格·基谢廖夫)

按照你的简短,简洁,实用的指示:

理解monad的最简单方法是在上下文中应用/组合函数。假设您有两个计算,它们都可以看作两个数学函数fg

  • f获取一个String并生成另一个String(获取前两个字母)
  • g接受一个String并生成另一个String(大写转换)

因此,在任何语言中,转换“获取前两个字母并将其转换为大写”都将写入g(f(“一些字符串”))。因此,在纯完美函数的世界中,组合就是:做一件事,然后做另一件事。

但是假设我们生活在可能失败的函数世界中。例如:输入字符串可能有一个字符长,所以f会失败。所以在这种情况下

  • f接受一个String并生成一个String或无。
  • 只有当f没有失败时,g才会产生一个String。否则,不产生任何东西

所以现在,g(f(“一些字符串”))需要一些额外的检查:“计算f,如果它失败了,那么g应该返回无,否则计算g”

这个想法可以应用于任何参数化类型,如下所示:

设Context[Someype]是背景同型的计算

  • f:: AnyType -> Context[Sometype]
  • g:: Sometype -> Context[AnyOtherType]

组合g(f())应该读作“计算f。在这个上下文中做一些额外的计算,然后计算g,如果它在上下文中有意义”

单子是一个带有特殊机器的盒子,它允许您从两个嵌套的盒子中制作一个普通盒子-但仍保留两个盒子的一些形状。

具体来说,它允许您执行类型Monad m => m (m a) -> m ajoin

它还需要一个return操作,它只是包装了一个值。return :: Monad m => a -> m a
你也可以说join解箱和return包装——但是joinMonad m => m a -> a类型的没有(它不会解包所有的单子,它会解包单子,里面有单子。)

所以它需要一个单子框(Monad m =>m),里面有一个盒子((m a)),并制作一个普通的盒子(m a)。

然而,Monad通常用于(>>=)(口语“bind”)运算符,它本质上只是fmapjoin在一起。具体地说,

x >>= f = join (fmap f x)(>>=) :: Monad m => (a -> m b) -> m a -> m b

请注意,该函数位于第二个参数中,而不是fmap

此外,join = (>>= id)

为什么这很有用?本质上,它允许您制作将操作串在一起的程序,同时在某种框架(Monad)中工作。

Haskell中Monad最突出的用途是IO Monad。
现在,IO是Haskell中对行动进行分类的类型。在这里,Monad系统是保存(大花哨单词)的唯一方法:

  • 参考透明度
  • 懒惰
  • 纯度

本质上,像getLine :: IO String这样的IO操作不能被String替换,因为它总是有不同的类型。把IO想象成一种神奇的盒子,可以把东西传送给你。
然而,仍然只是说getLine :: IO String和所有函数都接受IO a会造成混乱,因为可能不需要这些函数。const "üp§" getLine会怎么做?(const丢弃了第二个参数。const a b = a。)getLine不需要被评估,但它应该做IO!这使得行为相当不可预测-也使得类型系统不那么“纯粹”,因为所有函数都将接受aIO a值。

输入IO Monad。

要将动作串在一起,您只需展平嵌套的动作。
要将函数应用于IO操作的输出,IO a类型中的a,您只需使用(>>=)

例如,输出输入的行(输出行是一个产生IO操作的函数,与>>=的右参数匹配):

getLine >>= putStrLn :: IO ()-- putStrLn :: String -> IO ()

这可以用do环境更直观地编写:

do line <- getLineputStrLn line

本质上,像这样的do块:

do x <- ay <- bz <- f x yw <- g zh xk <- h zl k w

…变成了这样:

a     >>= \x ->b     >>= \y ->f x y >>= \z ->g z   >>= \w ->h x   >>= \_ ->h z   >>= \k ->l k w

还有m >>= \_ -> f>>运算符(当框中的值不需要在框中创建新框时)也可以写成a >> b = a >>= const bconst a b = a

此外,return运算符是以IO-直觉为模型的-它返回一个带有最小上下文的值,在这种情况下是没有IO。由于IO a中的a代表返回的类型,这类似于命令式编程语言中的return(a)-但它确实没有停止了动作链!f >>= return >>= g相同作为f >>= g。只有当您返回的术语在链中较早创建时才有用-见上文。

当然,还有其他的Monad,否则它就不会被称为Monad,它会被称为“IO Control”之类的东西。

例如,List Monad(Monad [])通过连接-使(>>=)运算符对列表的所有元素执行函数来变平。这可以被视为“不确定性”,其中List是许多可能的值,Monad框架正在进行所有可能的组合。

例如(在GHCi中):

Prelude> [1, 2, 3] >>= replicate 3  -- Simple binding[1, 1, 1, 2, 2, 2, 3, 3, 3]Prelude> concat (map (replicate 3) [1, 2, 3])  -- Same operation, more explicit[1, 1, 1, 2, 2, 2, 3, 3, 3]Prelude> [1, 2, 3] >> "uq""uququq"Prelude> return 2 :: [Int][2]Prelude> join [[1, 2], [3, 4]][1, 2, 3, 4]

因为:

join a = concat aa >>= f = join (fmap f a)return a = [a]  -- or "= (:[])"

如果发生这种情况,也许Monad只会将所有结果取消为Nothing。也就是说,绑定自动检查函数(a >>=f)是否返回或值(a>>= f)是否为Nothing-然后也返回Nothing

join       Nothing  = Nothingjoin (Just Nothing) = Nothingjoin (Just x)       = xa >>= f             = join (fmap f a)

或者更明确地说:

Nothing  >>= _      = Nothing(Just x) >>= f      = f x

State Monad用于修改某些共享状态的函数-s -> (a, s),因此>>=的参数是:: a -> s -> (a, s)
这个名字是一种用词不当,因为State实际上是用于修改状态的函数,而不是用于状态-状态本身真的没有有趣的属性,它只是被改变了。

例如:

pop ::       [a] -> (a , [a])pop (h:t) = (h, t)sPop = state pop   -- The module for State exports no State constructor,-- only a state function
push :: a -> [a] -> ((), [a])push x l  = ((), x : l)sPush = state push
swap = do a <- sPopb <- sPopsPush asPush b
get2 = do a <- sPopb <- sPopreturn (a, b)
getswapped = do swapget2

然后:

Main*> runState swap [1, 2, 3]((), [2, 1, 3])Main*> runState get2 [1, 2, 3]((1, 2), [1, 2, 3]Main*> runState (swap >> get2) [1, 2, 3]((2, 1), [2, 1, 3])Main*> runState getswapped [1, 2, 3]((2, 1), [2, 1, 3])

还有:

Prelude> runState (return 0) 1(0, 1)

MonadApplicative(即您可以提升二进制-因此,“n-ary”-函数,(1)并将纯值注入(2)Functor(即您可以地图翻转的东西,(3)即将Applicative0函数提升到(3)),并增加了Applicative2的能力(三个概念中的每一个都遵循其相应的Applicative3集合)。在Haskell中,这种扁平化操作称为join

这个"#0"操作的一般(通用,参数)类型是:

join  ::  Monad m  =>  m (m a)  ->  m a

对于任何monadm(NB类型中的所有m都是相同的!)。

特定的m monad定义了其特定版本的join,适用于类型m a的monadic值“携带”的任何值类型a。一些特定类型是:

join  ::  [[a]]           -> [a]         -- for lists, or nondeterministic valuesjoin  ::  Maybe (Maybe a) -> Maybe a     -- for Maybe, or optional valuesjoin  ::  IO    (IO    a) -> IO    a     -- for I/O-produced values

join操作将产生#3类型值的#1计算m计算转换为a类型值的m计算组合。这允许将计算步骤组合成一个更大的计算。

这个计算步骤组合"绑定">>=)运算符简单地将fmapjoin一起使用,即:

(ma >>= k)  ==  join (fmap k ma){-ma        :: m a            -- `m`-computation which produces `a`-type valuesk         ::   a -> m b     --  create new `m`-computation from an `a`-type valuefmap k ma :: m    ( m b )   -- `m`-computation of `m`-computation of `b`-type values(m >>= k) :: m        b     -- `m`-computation which produces `b`-type values-}

相反,join可以通过bind定义,join mma == join (fmap id mma) == mma >>= id其中id ma = ma——对于给定类型m更方便的那个。

对于monad,#0符号和等效的绑定使用代码,

do { x <- mx ; y <- my ; return (f x y) }        --   x :: a   ,   mx :: m a--   y :: b   ,   my :: m bmx >>= (\x ->                                    -- nestedmy >>= (\y ->                        --  lambdareturn (f x y) ))       --   functions

可以理解为

首先“做”mx,完成后,将其“结果”作为x,让我用它来“做”其他事情。

在给定的do块中,绑定箭头<-右侧的每个值对于某些类型a和整个do块中的相同monadm都是m a类型。

return x是一个中性的m计算,它只是产生它所给出的纯值x,因此将任何m计算与return绑定根本不会改变该计算。


(1)liftA2 :: Applicative m => (a -> b -> c) -> m a -> m b -> m c

(2)pure :: Applicative m => a -> m a

(3)fmap :: Functor m => (a -> b) -> m a -> m b

还有等价的Monad方法,

liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m creturn :: Monad m =>  a            -> m aliftM  :: Monad m => (a -> b)      -> m a -> m b

给定一个monad,其他定义可以是

pure   a       = return afmap   f ma    = do { a <- ma ;            return (f a)   }liftA2 f ma mb = do { a <- ma ; b <- mb  ; return (f a b) }(ma >>= k)     = do { a <- ma ; b <- k a ; return  b      }

根据当我们谈论单子时我们在谈论什么,问题“什么是monad”是错误的:

“什么是monad?”这个问题的简短回答是,它是内函子范畴中的一个monid,或者它是一个通用数据类型,配备了两个满足某些定律的操作。这是正确的,但它没有揭示一个重要的大局。这是因为这个问题是错误的。在本文中,我们旨在回答正确的问题,即“当作者谈论monad时,他们真正在说什么?”

虽然这篇论文没有直接回答什么是monad,但它有助于理解不同背景的人在谈论monad时的意思以及为什么。

monad是一个容器,但用于数据。一个特殊的容器。

所有的容器都可以有开口和把手和喷口,但这些容器都保证有一定的开口和把手和喷口。

为什么?因为这些有保证的开口、把手和喷口对于以特定的、常见的方式拾取和连接容器是有用的。

这允许您选择不同的容器,而无需了解它们。它还允许不同类型的容器轻松链接在一起。

对于来自命令式背景的人(特别是c#),

考虑以下代码

bool ReturnTrueorFalse(SomeObject input){if(input.Property1 is invalid){return false;}
if(input.Property2 is invalid){return false;}
DoSomething();return true;}

你会看到很多这样的代码,你甚至会看到没有早期回报,但所有的检查都是嵌套完成的。现在,Monad是一种模式,它可以像下面这样扁平化

Monad<bool> ReturnTrueorFalse(SomeObject input) =>from isProperty1Valid in input.Property1from isProperty2Valid in input.Property2select Monad.Create(isProperty1Valid && isProperty2Valid);

这里有几件事要注意。首先,函数的返回值发生了变化。其次,输入的两个属性都必须是Monad。接下来,Monad应该实现Select许多(LINQ的扁平化运算符)。由于Select许多是为该类型实现的,因此可以使用查询语法编写语句

那么,什么是Monad?它是一种结构,它以可组合的方式展平返回相同类型的表达式。这在函数式编程中特别有用,因为大多数函数式应用程序倾向于将状态和IO保留在应用程序的边缘层(例如:控制器),并在整个调用堆栈中返回基于Monad的返回值,直到需要解包该值。当我第一次看到它时,对我来说最大的好处是它是如此简单,因为它是如此的声明性。

每个c#(现在几乎每个人)开发人员都可以立即识别的Monad的最佳示例是async/wait。之前. Net4.5我们必须使用ContinueWith编写基于任务的语句来处理回调,在async/wait之后,我们开始使用异步语法的同步语法。这是可能的,因为任务是一个“monad”。

参考这个获得详细的解释,这个获得简单的实现,语言分机获得许多令人敬畏的Monad和大量关于OOP开发人员的函数式编程的信息