什么是单子?

问题描述 投票:1327回答:45

在简要看着哈斯克尔最近,这将是一个简短,简洁,实用的解释,以什么单子基本上是什么?

我发现大部分的解释我遇到是相当不可访问,并在实际缺少细节。

haskell functional-programming monads terminology
45个回答
1027
投票

首先:术语单子有点空洞,如果你不是一个数学家。另一种期限的计算建设者这是一个有点更具描述性的东西,他们是真正有用的。

你问到我实际的例子:

实施例1:列表理解:

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

此表达式返回所有的奇数的双打中从1到10非常有用的范围内!

原来,这是真正为列表单子中的一些操作只是语法糖。同样的列表理解可以写成:

do
   x <- [1..10]
   guard (odd x)
   return (x * 2)

甚至:

[1..10] >>= (\x -> guard (odd x) >> return (x*2))

实施例2:输入/输出:

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

这两个例子都使用单子,AKA计算建设者。共同的主题是,在一些特定的,有用的方法单子链操作。在列表理解的操作时,链接等,如果操作返回一个列表,那么下面的操作列表中的每一项执行。在另一方面所述IO单子执行的操作顺序,但是通过一个“隐藏变量”沿,其表示,这使我们能够写I / O的代码在纯功能性的方式“世界的状态”。

原来链运作的模式是非常有用的,用于大量的在Haskell不同的事情。

另一个例子是例外:使用Error单子,操作被链接,使得它们除如果引发错误,在这种情况下,链的其余部分被放弃顺序地执行。

这两个名单,理解语法和DO-符号是使用>>=操作链操作语法糖。单子基本上是支持>>=操作类型。

实施例3:甲解析器

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

parseExpr = parseString <|> parseNumber

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

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

操作chardigit等非常简单。他们要么匹配或不匹配。神奇的是管理控制流程的单子:该操作直至匹配失败,在这种情况下单子回溯到最新<|>并尝试下一个选项顺序进行。此外,有一些额外的,有用的语义链运作的一种方式。

实施例4:异步编程

上述的例子是在Haskell,但事实证明,F#还支持单子。这个例子是从Don Syme被盗:

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单子能够“分裂”块自身和推迟后半期的执行。 (该async {}语法指示使用了该块中的控制流程是由async单子所定义。)

他们是如何工作的

因此,一个单子怎么可以做所有这些花哨的控制流的事情吗?究竟发生在一个做块(或计算表达式因为他们是所谓的F#),是每一个操作(基本上每线)封装在一个单独的匿名函数。然后,这些功能所使用的操作者bind(在Haskell拼写>>=)相结合。由于bind操作结合的功能,它可以其认为合适的执行它们:顺序,多次,在倒车时,丢弃一些,在一个单独的线程中执行一些时,感觉就像它等等。

作为一个例子,这是实施例2的IO-码的扩展版本:

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

这是难看,但它也比较明显什么是真正回事。该>>=运算符是神奇的成分:它需要一个值(左侧)和具有功能(右侧)相结合的,以产生新的价值。那么这个新的值取到下>>=操作,并再次结合了功能,产生新的价值。 >>=可以被看作是一个小型的评估器。

需要注意的是>>=超载针对不同类型的,所以每一个单子都有自己的执行>>=的。 (链中的所有操作都必须相同单子的类型,虽然,否则>>=运营商将无法正常工作。)

最简单的可能的实现>>=的只是取值在左边,它适用于右边的功能,并返回结果,但在此之前如说,是什么让整个模式有用的是,当有额外的东西在单子的实施回事的>>=

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

加起来

在Haskell-术语一个单子是参数化类型是所述类型单子类,它定义>>=与几个其他运营商沿的一个实例。通俗地说,一个单子就是被定义为其>>=操作的类型。

在本身>>=是链接功能只是一个麻烦的方式,但随着DO-符号,它可以隐藏“管道”的存在,在一元业务原来是一个非常好的和有用的抽象,语言中的许多有用的地方,和在语言创建自己的迷你语言有用。

为什么单子难?

对于许多哈斯克尔学习者,单子是他们打犹如一道砖墙的障碍。这并不是说单子本身是复杂的,但该实现依赖于许多其他先进的Haskell功能,如参数化类型,类型类,等等。问题是,Haskell的I / O是基于单子,和I / O可能是你想学习一门新语言时,要了解的第一件事情 - 毕竟,这不是很有趣创建不产生任何程序输出。我对这个鸡和蛋的问题没有立即解决,除了像“魔术在这里发生”,直到你跟语言的其他部位足够的经验,处理I / O。抱歉。

单子上的优秀博客:http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html


34
投票

甲单子是,有效地,“类型的操作”的一种形式。它会做三两件事。首先,它会“包装”(或以其他方式转换)一种类型的值成另一种类型(通常称为“一元类型”)。其次,它将使所有的操作(或功能)可在基础类型的单子类型可用。最后它将为自身与另一个单子相结合,产生复合单子的支持。

该“也许单子”基本上是在Visual Basic / C#“可空类型”的等价物。它需要一个非空类型“T”,并把它转换成一个“可空<T>”,然后定义了所有二进制运算符上的可空<T>的意思。

副作用是simillarly表示。的结构创建保存的副作用旁边一个函数的返回值的描述。在“解禁”操作,那么各地的副作用复制为值的函数之间传递。

他们被称为“单子”,而不是“经营型”的几个原因的更容易掌握的名称:

  1. 单子有(详见定义),他们可以做什么限制。
  2. 这些限制,与事实有涉及三个操作一起,顺应事物的结构,称为范畴论单子,这是数学的一个不起眼的分支。
  3. 他们由“纯”函数语言的拥护者设计
  4. 像数学的分支晦涩纯函数式语言的拥护者
  5. 因为数学是模糊的,而且单子与特定的编程风格有关,人们往往用这个词单子作为一种秘密的握手。正因为如此没有一个一直困扰着投资于一个更好的名字。

34
投票

几年前,给人一种回答这个问题后,我相信我可以改进和简化与响应...

甲单子是一个功能的组合物的技术,用于使用构图功能一些输入场景外部化处理,bind,以组合物中预处理输入。

在正常的组合物中,功能,compose (>>),是用于该合成的功能应用到其在序列前身的结果。重要的是,需要被组成的函数来处理其输入的所有方案。

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

这样的设计可以通过重组输入,使得相关的状态更容易地询问被改善。因此,而不是简单地y值可以成为Mb比如,举例来说,如果(is_OK, b) y包括有效性的概念。

例如,当输入是唯一可能的数,而不是返回其可以含有忠实地包含多个与否,可以重组型成表示有效的数字的存在和数量的元组,如bool字符串, bool * float。将所组成的功能现在将不再需要解析的输入字符串来确定一个数字是否存在,但可能只是检查一个元组的bool部分。

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

在这里,同样,组成自然地发生compose等各功能必须处理其单独输入的所有场景,但这样做是现在容易得多。

然而,如果我们能有什么外部化审讯那些时期,其中处理一个场景是例行的工作。例如,如果有什么我们的程序不执行任何操作,当输入不是OK作为当is_OKfalse。如果这样做了,然后由职能将不会需要处理那种情况下自己,大大简化了他们的代码,并实现重用的另一个层次。

为了实现这一外化,我们可以使用一个函数,bind (>>=),执行compositioncompose代替。因此,代替简单地从一个函数的输出值传送到另一Bind将检查MMa部,并决定是否以及如何将组成函数应用于a的输入端。当然,功能bind会专门为我们特别M定义,以便能够检查其结构和执行我们想要的任何类型的应用。尽管如此,a可以是任何东西,因为仅仅bind未经检查传递到所构成的功能时,它确定需要应用a。此外,所构成的函数本身不再需要任一处理输入结构的M部分,从而简化了它们。因此...

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

总之,一个单子外部化,从而提供围绕的某些输入场景一旦输入变得设计为充分地暴露它们的治疗标准行为。这种设计是shell and content模型,其中壳含有相关的组成功能的应用数据,并通过审问,仍只提供给bind功能。

因此,一个单子就是三两件事:

  1. 一个M外壳保持单子的相关信息,
  2. 一个bind功能实现为利用其的功能组成的含量值(一个或多个)应用此壳信息找到的外壳内,并且
  3. 形式,a -> Mb的组合的功能,产生的结果,其中包括单子管理数据。

一般来说,输入到函数比它的输出可以包括诸如错误情况更加限制性的;因此,该结果Mb结构通常是非常有用的。例如,当除数为0除法运算符不返回一个数字。

此外,monads可包括涡卷函数涡卷值,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 a
Right Identity : Ma >>= return    === Ma
Associative    : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)

Associativity意味着无论当施加bind的那bind保留评价的顺序。也就是说,在Associativity上文定义,binding的括号f力早期评估和g只会导致一个期望Ma为了完成bind的功能。因此必须确定Ma的评估之前,可以成为施加其值以f并且该结果又施加到g


24
投票

单子是控制流什么抽象数据类型的数据。

换句话说,许多开发人员都熟悉的集合,列表,字典(或哈希或映射),和树的想法。在这些数据类型有许多特殊情况下(例如InsertionOrderPreservingIdentityHashMap)。

然而,当有程序“流”面临许多开发商都没有接触过许多比若再结构,开关/箱,做,同时,后藤(GRR),以及(也许)关闭。

因此,一个单子是一个简单的控制流结构。一个更好的词来代替单子是“控制型”。

因此,一个单子有控制逻辑,或声明,或功能插槽 - 在数据结构相当于是说,有些数据结构允许你添加数据,然后将其取下。

例如,“如果”单子:

if( clause ) then block

在其最简单的具有两个槽 - 的条款,和一个块。该if单子通常是建立评估条款的结果,如果不是假的,评估该块。许多开发人员都没有引入单子时,他们学习“如果”,它只是没有必要了解单子写的有效逻辑。

单子会变得更加复杂,以同样的方式,数据结构会变得更加复杂,但也有可能具有相似的语义单子很多大类,但不同的实现方式和语法。

当然,在相同的方式,数据结构可以被遍历,或穿过,单子可被评估。

编译器可能会或可能不会有用户定义的单子支持。哈斯克尔当然不会。伊欧凯有一些类似的功能,尽管该术语单子不使用的语言。


14
投票

我最喜欢的单子教程:

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

(出在谷歌搜索“单子教程” 170000次命中!)

@Stu:单子的点是允许添加(通常)顺序语义到否则纯代码;你甚至可以撰写单子(Monad的使用变压器),并获得更多有趣和复杂的组合的语义,比如错误处理,共享状态和日志分析,例如。所有这一切都是可能的纯代码,单子只让你将它抽象掉,并在模块库中重复使用它(在编程总是好的),以及提供方便的语法使它看起来势在必行。

哈斯克尔已经运算符重载[1]:它使用类型类多一个可以使用Java或接口的方式C#,但哈斯克尔恰好也允许像+ &&非字母数字标记和>为缀标识符。这只是在你看它的方式过载,如果你的意思是“超载的分号”操作[2]。这听起来像巫术和要求麻烦“重载分号”(图进取的Perl黑客得到这个想法的风),但问题是,没有单子,没有分号,因为纯粹的功能代码不要求或允许明确的测序。

这一切听起来复杂得多它需要。 SIGFPE的文章是很酷,但使用哈斯克尔来解释它,这有点无法打破的理解哈斯克尔神交单子和理解单子神交哈斯克尔鸡和蛋的问题。

[1]这是从单子一个独立的问题,但单子使用Haskell的操作符重载功能。

[2]这也是因为运营商的链接一元行动过于简单化是>> =(发音为“绑定”),但有语法糖(“做”),让您使用括号和分号和/或缩进和换行符。


9
投票

我以不同的方式在思考单子,最近。我一直在思考他们在数学的方式,这使得新种多态性可能的抽象出来执行顺序。

如果您使用的是命令式语言,你写一些表达,从而,代码始终运行完全相同的顺序。

而在简单的情况下,当你使用一个单子,那感觉是相同的 - 你定义,以便发生表达式的列表。不同之处在于,这取决于你使用的单子,你的代码可能会为了(如在IO单子)同时运行,以在多个项目并行(就像在列表单子),它可能通过中途停止(像也许单子) ,它可能会暂停在中途经过稍后恢复(如在恢复单子),它可能会倒带,从头开始(如交易中的单子),也可能倒退中途(在逻辑单子等)尝试其他选项。

而且由于单子是多态的,有可能在不同的单子上运行相同的代码,根据您的需要。

另外,在某些情况下,它可能单子结合在一起(与单子变压器),同时获得多种功能。


9
投票

我还是新的单子,但我想我会分享一个链接,我发现感觉真的很好看(有图片!!):http://www.matusiak.eu/numerodix/blog/2012/3/11/monads-for-the-layman/(无隶属关系)

基本上,温暖和模糊的概念,我从文章得到的是,单子基本上适配器允许不同的功能,以组合的方式工作的概念,即能够串起来的多种功能,并无需担心不一致回报混合和匹配种类和。所以,BIND的功能是负责保持与苹果和橘子与苹果,当我们正在努力使这些适配器的橘子。和升降功能是负责以“低级别”功能和“升级”他们与BIND功能工作,可组合为好。

我希望我得到了它的权利,更重要的是,希望本文对单子有效的视图。如果不出意外,这篇文章帮助磨我的胃口了解更多有关单子。


8
投票

除了以上的出色的答案,让我为您提供一个链接到下面的文章(由帕特里克·汤姆逊),其通过与概念的JavaScript库jQuery的(和它使用“方法链”操作DOM的方式)解释单子:jQuery is a Monad

jQuery documentation本身并不指“单子”一词,但谈到了“构建者模式”,这可能是比较熟悉的。这不会改变,你有一个正确的单子有可能甚至没有意识到它的事实。


8
投票

Monads Are Not Metaphors,但实际上一个有用的抽象,从一个共同的模式不断涌现,如丹尼尔Spiewak解释。


6
投票

甲单子是组合在一起的计算共享公共上下文的方法。它就像建设管网。在构建网络时,有流过任何数据。但是,当我已经完成拼接的所有位与“绑定”和“回归”在一起,然后我祈求像runMyMonad monad data和数据流经管道。


6
投票

在实践中,单子是功能组合物操作的自定义实现该负责的副作用和不相容的输入和返回值(链接)。


699
投票

解释“什么是单子”是有点像说:“什么是多少?”我们使用数字的所有时间。但是想象一下,你遇到的人谁不知道什么数字。你会如何赫克解释的数字是什么?怎么会,你甚至开始来形容为什么这可能是有用的?

什么是单子?答案很简单:这是链接一起操作的特定方式。

从本质上说,你写的执行步骤,并与“绑定功能”他们连接起来。 (在Haskell,它的命名>>=。)你可以调用自己编写的绑定运营商,或者您可以使用语法糖,这使得编译器插入你的函数调用。但无论哪种方式,每个步骤通过该绑定函数的调用分开。

所以绑定功能就像是一个分号;其分离的方法中的步骤。绑定功能的工作是采取从上一步骤的输出,并送入下一个步骤。

这听起来太狠了吧?但有不止一种单子。为什么?怎么样?

那么,绑定功能,可以只取结果的一步,并且将其提供给下一个步骤。但如果这“一切”的单子呢?实际上并不是非常有用的。这理解是很重要的:每一个有用的单子做别的事情,除了只是作为一个单子。每一个有用的单子有“特殊权力”,这使得它独一无二的。

(即没有什么特别之处就是所谓的“身份单子”单子。而不是像身份的功能,这听起来像一个毫无意义的事情,但事实证明并非是......但是,这是另一个故事™)。

基本上,每个单子都有自己的实现绑定功能。你可以写一个绑定功能,使得它的执行步骤之间hoopy事情。例如:

  • 如果每一步返回成功/失败指示灯,您可以执行绑定仅在上一个成功的下一个步骤。通过这种方式,一个失败的步骤中止“自动”整个序列,却没有你的条件测试。 (的失败单子。)
  • 扩展这个想法,你可以实现“例外”。 (误差单子或异常单子。)因为你自己定义它们,而不是它是一个语言功能,您可以定义它们是如何工作。 (例如,也许你想忽略前两个例外,只有当第三抛出异常中止。)
  • 你可以让每一个步骤返回多个结果,并具有绑定功能遍历它们,喂每一个到你的下一个步骤。通过这种方式,您不必继续写作圈所有的地方有多个结果的时候。绑定功能“自动”做一切你。 (名单单子。)
  • 除了传递一个“结果”,从一步到另一步,你可以有各地的绑定功能,通过额外的数据也是如此。这个数据现在并不在你的源代码显示出来,但你仍然可以从任何地方访问它,而无需手动将它传递给每一个功能。 (读者单子。)
  • 你可以让这个“额外数据”可以被替换。这使您可以模拟破坏性的更新,而无需实际做破坏性的更新。 (国家单子和它的堂兄作家单子。)
  • 由于您只模拟破坏性的更新,你可以平凡做的事情,那是不可能与真正的破坏性更新。例如,您可以撤消的最后一次更新,或恢复到旧版本。
  • 你可以做一个单子,其中计算可以暂停,所以你可以暂停程序,进去和内部状态数据鼓捣,然后恢复它。
  • 您可以实现“延续”的单子。这使您可以打破人们心中!

所有的这一切,更是可能的单子。当然,这一切还没有单子完全可以了。这只是大幅容易使用的单子。


5
投票

如果我理解正确的话,IEnumerable的是从单子的。我不知道这可能是方法一个有趣的角度对我们这些从C#世界?

对于它的价值,这里有一些教程的链接,帮助我(没有,我还没有明白是什么单子)。


5
投票

这两件事,学习的时候大约有最好的帮助我:

第8章“功能分析器,”从格雷厄姆赫顿的书Programming in Haskell。这完全不提单子,其实,但如果你可以通过章工作,真正了解它的一切,绑定操作序列特别是如何评价,你就会明白单子的内部。预计这将需要多次尝试。

本教程All About Monads。这使它们的使用几个很好的例子,我不得不说,在Appendex比喻我为我工作。


5
投票

幺似乎是东西,确保在半群和支持的类型定义的所有操作都将永远是半群内返回一个支持的类型。例如,任何数量的+任何数量= A号码,没有错误。

而除法接受两个小数格式,并返回一个分数,其通过零在Haskell somewhy(这恰好是一个分数somewhy)定义分裂为无限...

在任何情况下,它似乎单子只是一种方式,以确保您的业务链,可预测的方式表现,并声称是民的功能 - >民,与数量 - >民的另一个功能由被称为与x不比方说,火导弹。

在另一方面,如果我们有一个函数,它不火导弹,我们可以与其他功能组成它也火导弹,因为我们的目的是明确的 - 我们想火导弹 - 但它不会尝试打印“Hello World”的一些奇怪的原因。

在Haskell中,主要类型为IO(),或IO的[()]的区别是奇怪的,我不会讨论它,但这里是我想发生:

如果我有主,我想要它做的动作环环相扣,我运行程序的原因是为了产生影响 - 虽然通常IO。因此,我可以链IO操作的主在一起,以 - 做IO,没有别的。

如果我尝试做一些不“返回IO”,程序会抱怨链不流动,或基本“请问这个涉及到我们正在尝试做的 - 一个IO动作”,这似乎给力程序员保持自己的思路,没有远走高飞,并考虑发射导弹,而排序算法创造 - 这不流动。

基本上,单子似乎是一个提示到:“嘿,你知道这个功能,这里返回一个数字的编译器,它实际上并不总是有效,有时可能会生成一些,有时什么都没有,只是记住这心神”。知道了这一点,如果你试图断言单子动作,单子动作可以作为一个编译时异常说:“哎,这实际上不是一个数字,这可能是一个数字,但你不能假设这一点,做一些事情以确保该流动是可接受的“。这防止不可预知的程序行为 - 进行公正的程度。

它出现单子不是关于纯度,也不控制,但约保持在其上的所有行为是可预测的和所定义,或不编译的类别的标识。你不能做什么,当你希望做一些事情,如果你预计什么也不做(可见)你不能做什么。

我能为单子想到的最大的原因是 - 去看看程序/ OOP代码,你会发现,你不知道节目开始的地方,也没有结束,你看到的是大量的跳跃和大量的数学,魔术和导弹。您将无法保持它,如果可以的话,你会花相当多的时间来包装一下你心目中的整个程序周围,然后才能了解它的任何一部分,因为在这种情况下模块是基于interdependant“节”的代码,其中代码被优化,以尽可能相关效率/相互关系的承诺。单子是非常具体的,并且通过定义明确的,并确保程序的流动是可能的分析,和隔离这是难于分析部件 - 因为它们本身是单子。单子似乎是一个“可理解单元,其在充分理解预见的” - 如果你理解“也许”的单子,有没有可能的方式将做,除非是“也许”,这似乎微不足道的事情,但在大多数非一元代码,功能简单的“HelloWorld”可以发射导弹,什么也不做,或毁灭宇宙,甚至扭曲的时间 - 我们不知道,也没有任何保证,它是它是什么。单子保证,它是它是什么。这是非常强大的。

在“现实世界”所有的事情似乎是单子,在某种意义上说,它是通过一定的法律观察到防止混乱的约束。这并不意味着我们要模仿这个对象来创建类的所有操作,而不是我们可以简单地说“方是一个正方形”,不过是一个正方形,即使不是矩形,也不是一个圆圈,和“广场上有面积它的一个长度的现有尺寸乘以本身。无论你有什么样方,如果它在二维空间中的正方形,它的面积绝对不可能是任何东西,但它的长度的平方,它几乎是微不足道的证明,这是因为很强大我们并不需要作出的断言,以确保我们的世界会是现在的样子,我们只是用现实的影响,以防止我们的节目脱落轨道。

林几乎保证是错的,但我认为这可能帮助别人在那里,所以希望它可以帮助别人。


5
投票

在斯卡拉的情况下,你会发现下面是最简单的定义。基本上flatMap(或绑定)是“关联”和存在一个标识。

trait M[+A] {
  def flatMap[B](f: A => M[B]): M[B] // AKA bind

  // Pseudo Meta Code
  def isValidMonad: Boolean = {
    // for every parameter the following holds
    def 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 holds
    def isAnIdentity[X](x: M[X], id: X => M[X]): Boolean =
      x.flatMap(id) == x
  }
}

EG

// These could be any functions
val f: Int => Option[String] = number => if (number == 7) Some("hello") else None
val 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 are
scala> 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 identical
scala> Some(7).flatMap(id)
res213: Option[Int] = Some(7)

scala> Some(7)
res214: Some[Int] = Some(7)

注:严格地说一个Monad in functional programming的定义是不一样的Monad in Category Theory,这是在mapflatten的圈中定义的定义。虽然他们是在一定的映射一种等价的。这个演讲是非常好的:http://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category


5
投票

这个答案始于一个激励为例,通过示例工作,推导出单子的例子,并正式将“单子”。

考虑伪这三个功能:

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)为“饲料mf”。为了养活一对<x, messages>到一个函数fx进入f,得到<y, message>f的,并返回<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

你有最单子的。现在你只需要知道在你的程序中的数据类型。

什么类型的值是<x, "called f. ">?嗯,这要看是什么类型的值x的。如果x是类型t的,那么你对类型是“对t和串线”的值。呼叫类型M t

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

恭喜你,你已经创建了一个单子!

从形式上看,你的单子是元组<M, feed, wrap>

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

  • M是一种类型的构造。
  • feed花费(函数,它接受一个t并返回一个M u)和M t并返回M u
  • wrap需要v并返回一个M v

tu,和v任何三种类型,其可以是或可以不是相同的。单子满足你证明了你的具体单子三个属性:

  • 供给包裹t成一个函数是相同的传递展开t到函数。 形式上:feed(f, wrap(x)) = f(x)
  • 饲养的M twrap无助于M t。 形式上:feed(wrap, m) = m
  • 饲养的M t(称之为m)到一个函数 经过tg 从得到的M u n(称之为g) 饲料nf 是相同的 喂养mgn越来越g 喂养nf 形式上:feed(h, m) = feed(f, feed(g, m))其中h(x) := feed(f, g(x))

典型地,feed称为bind(AKA >>=在Haskell)和wrap称为return


5
投票

我会尽量在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所期待的。

我们可以组成f并与g操作.,但目前尚无法构成f1g1 .。问题是,我们不能直接地在背景值传递给一个期望的值,是不是在上下文的功能。

那岂不是很好,如果我们引入一个运算符组成g1f1,这样我们就可以写(f1 OPERATOR g1) xg1返回一个上下文的值。该值将被断章取义并应用于f1。是的,我们有这样的运营商。这是<=<

我们也有>>=运营商,对我们做同样的事情,但在一个稍微不同的语法。

我们写:g1 x >>= f1g1 xMaybe Int值。该>>=操作有助于采取Int值了“也许 - 不存在”断章取义,并将其应用于f1f1的结果,这是一个Maybe Bool,将是整个>>=操作的结果。

最后,为什么Monad有用吗?因为Monad是类型类定义>>=操作,非常的相同定义Eq==运营/=类型的类。

总括而言,Monad类型的类定义>>=运营商,使我们能够传递一个上下文(我们称这些单子值)不希望在上下文中值的函数值。内容将会得到照顾。

如果有一两件事要记住,它是Monads允许涉及上下文值函数组合。


5
投票

TL;博士

{-# LANGUAGE InstanceSigs #-}

newtype Id t = Id t

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

序幕

功能应用操作$

forall a b. a -> b

被标准地定义

($) :: (a -> b) -> a -> b
f $ 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 identity
     id . g  =  g            :: b -> c   Left identity
(f . g) . h  =  f . (g . h)  :: a -> d   Associativity

.是关联的,并且id是其左,右的身份。

三重Kleisli

在编程中,一个单子是一个仿函数类型构造函数与单子类型的类的一个实例。有定义和实施工作的若干等价变体,每个携带单子抽象略有不同的直觉。

函子是一种f与仿函数类型类的实例类型构造* -> *

{-# LANGUAGE KindSignatures #-}

class Functor (f :: * -> *) where
   map :: (a -> b) -> (f a -> f b)

除了以下实施静态类型协议,函子型类的实例必须服从的代数函子法律forall f g.

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

仿函数的计算有型

forall f t. Functor f => f t

一个计算c r由上下文r内的结果c

一元一元功能或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 where
   return :: t -> m t
   (=<<)  :: (a -> m b) -> m a -> m b

infixr 1 =<<

所述Kleisli身份return是Kleisli箭头促进值t到单子上下文m。扩展或=<<施加Kleisli箭头a -> m b来计算m a的结果Kleisli应用。

Kleisli组合物<=<在延伸的条款所定义

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

infixr 1 <=<

<=<组成2个Kleisli箭头,将左边的箭头,右箭头的申请结果。

单子类型的类必须遵守法律单子的情况下,在Kleisli组成方面最优雅的说:forall f g h.

   f <=< return  =  f                :: c -> m d   Right identity
   return <=< 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,身份单子被定义

newtype Id t = Id t

instance Functor Id where
   map :: (a -> b) -> Id a -> Id b
   map f (Id x) = Id (f x)

instance Monad Id where
   return :: t -> Id t
   return = Id

   (=<<) :: (a -> Id b) -> Id a -> Id b
   f =<< (Id x) = f x

选项

一个选项类型

data Maybe t = Nothing | Just t

编码计算Maybe t即不一定产生一个结果t,计算可能“失败”。该选项单子被定义

instance Functor Maybe where
   map :: (a -> b) -> (Maybe a -> Maybe b)
   map f (Just x) = Just (f x)
   map _ Nothing  = Nothing

instance Monad Maybe where
   return :: t -> Maybe t
   return = Just

   (=<<) :: (a -> Maybe b) -> Maybe a -> Maybe b
   f =<< (Just x) = f x
   _ =<< Nothing  = Nothing

a -> Maybe b被施加到仅当Maybe a产生的结果的结果。

newtype Nat = Nat Int

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

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

自然数不低于减法关闭。

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

infixl 6 -?

选项单子涵盖异常处理的基本形式。

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

名单

名单单子,在列表类型

data [] t = [] | t : [t]

infixr 5 :

其添加剂幺操作“追加”

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

infixr 5 ++

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

instance Functor [] where
   map :: (a -> b) -> ([a] -> [b])
   map f (x : xs) = f x : map f xs
   map _ []       = []

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> [a] -> [b]
   f =<< (x : xs) = f x ++ (f =<< xs)
   _ =<< []       = []

扩展=<<会连接++所有列表[b]从Kleisli的应用f x导致箭头a -> [b][a]的元素到一个结果列表[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   =   []

在限定的而不是延伸=<<单子型类,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 b
   m >> k = m >>= \ _ -> k
   {-# INLINE (>>) #-}

   return :: a -> m a
   return = pure

为了简单起见,这说明使用类型的类层次结构

class              Functor f
class Functor m => Monad m

在Haskell,目前的标准层次结构

class                  Functor f
class Functor p     => Applicative p
class Applicative m => Monad m

因为这不仅是每一个单子一个函子,但每一个应用性是一个仿函数,每个单子是一个适用了。

使用列表单子,祈伪

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

大致翻译到DO块,

do a <- [1 .. 10]
   b <- [1 .. 10]
   let p = a * b
   guard (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 in
         guard (even p) >>       -- [ () | even p ] >>
            return p
      )
   )

做记号和单子内涵是嵌套绑定表达式语法糖。绑定运营商用于本地名称的单子结果具有约束力。

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 where
   fail  :: m t
   (<|>) :: m t -> m t -> m t

infixl 3 <|>

instance Additive Maybe where
   fail = Nothing

   Nothing <|> m = m
   m       <|> _ = m

instance Additive [] where
   fail = []
   (<|>) = (++)

其中fail<|>形成幺forall k l m.

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

fail是添加剂单子的吸收/消灭零元件

_ =<< fail  =  fail

如果

guard (even p) >> return p

even p为真,则所述保护产生[()],并且,通过>>的定义,本地常数函数

\ _ -> return p

被施加到结果()。如果为假,则该防护件产生列表单子的fail[]),其产生任何结果为Kleisli箭头>>要被施加到,所以这个p被跳过。

臭名昭著,单子用于编码状态计算。

甲状态处理器是一个函数

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

该转变的状态st并产生一个结果t。国家st可以是任何东西。没什么,标志,计数,阵列处理,机器,世界。

该型状态的处理器通常被称为

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

状态处理器单子是kinded * -> *算符State st。状态处理器单子Kleisli箭头是功能

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

在典型的Haskell,状态处理器单子的懒惰版本被定义

newtype State st t = State { stateProc :: st -> (t, st) }

instance Functor (State st) where
   map :: (a -> b) -> ((State st) a -> (State st) b)
   map f (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  (f x, s1)

instance Monad (State st) where
   return :: t -> (State st) t
   return x = State $ \ s -> (x, s)

   (=<<) :: (a -> (State st) b) -> (State st) a -> (State st) b
   f =<< (State p) = State $ \ s0 -> let (x, s1) = p s0
                                     in  stateProc (f x) s1

甲状态处理器通过供应的初始状态下运行:

run :: State st t -> st -> (t, st)
run = stateProc

eval :: State st t -> st -> t
eval = fst . run

exec :: State st t -> st -> st
exec = snd . run

国家访问是由元getput,抽象了状态单子的方法来提供:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}

class Monad m => Stateful m st | m -> st where
   get :: m st
   put :: st -> m ()

m -> st声明对单子st状态类型m的功能依赖;一个State t,例如,将确定被唯一t的状态类型。

instance Stateful (State st) st where
   get :: State st st
   get = State $ \ s -> (s, s)

   put :: st -> State st ()
   put s = State $ \ _ -> ((), s)

与类似地使用在C.到void单元类型

modify :: Stateful m st => (st -> st) -> m ()
modify f = do
   s <- get
   put (f s)

gets :: Stateful m st => (st -> t) -> m t
gets f = do
   s <- get
   return (f s)

gets经常与记录字段的访问器使用。

状态单子等效可变螺纹的

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

其中s0 :: Int,是同样引用透明,但无限更美观实用

(flip run) 34
   (do
      modify (+ 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                 }
}                    }

类似于面向表达编程(例如生锈),一个块的最后一个语句表示它的产率。绑定操作者有时被称为“可编程分号”。

从结构化命令性编程迭代控制结构图元monadically仿真

for :: Monad m => (a -> m b) -> [a] -> m ()
for f = foldr ((>>) . f) (return ())

while :: Monad m => m Bool -> m t -> m ()
while c m = do
   b <- c
   if b then m >> while c m
        else return ()

forever :: Monad m => m t
forever m = m >> forever m

输入输出

data World

该I / O世界状态处理器单子是纯的Haskell的和解和现实世界中,功能性和外延式势在必行操作语义。实际严格执行密切模拟:

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

互动是不纯的原语便利

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

的代码,使用图元IO杂质被永久地由类型系统protocolized。由于纯度是真棒,在IO发生什么事,留在IO

unsafePerformIO :: IO t -> t

或者,至少,应该。

一个Haskell程序的类型签名

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

扩展到

World -> ((), World)

是一种把世界的功能。

结语

类别whiches对象的Haskell类型和whiches态射是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)是同态类X -> Y所有态射C的。仿函数必须保留态射特性和组合物中,C的“结构”,在D

                    Tmor    Tobj

      T(id)  =  id        : T(X) -> T(X)   Identity
T(f) . T(g)  =  T(f . g)  : T(X) -> T(Z)   Composition

该Kleisli类或类别C由三Kleisli给定

<T, eta, _*>

一个endofunctor的

T : C -> C

f),身份态射etareturn),和延伸操作者*=<<)。

每个Kleisli射一Hask

      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 identity
   return <=< g  =  g                :: b -> m c

       f .T eta  =  f                :  Z -> T(U)   Right identity
   f <=< 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  =  g
     eta* . g  =  g               By definition of .T
     eta* . g  =  id . g          forall f.  id . f  =  f
         eta*  =  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

单子也可以不是Kleislian扩展定义的,而是自然转化mu,在节目叫join。甲单子在mu方面定义为三重以上的类别C,一个endofunctor的

     T :  C -> C
     f :: * -> *

和两个自然转化

   eta :  Id -> T
return :: t  -> f t

    mu :  T . T   -> T
  join :: f (f t) -> f t

满足等价

       mu . T(mu)  =  mu . mu               :  T . T . T -> T . T   Associativity
  join . map join  =  join . join           :: f (f (f t)) -> f t

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

单子类型的类被定义

class Functor m => Monad m where
   return :: t -> m t
   join   :: m (m t) -> m t

规范mu执行选项单子:

instance Monad Maybe where
   return = Just

   join (Just m) = m
   join Nothing  = Nothing

concat功能

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

在列表单子的join

instance Monad [] where
   return :: t -> [t]
   return = (: [])

   (=<<) :: (a -> [b]) -> ([a] -> [b])
   (f =<<) = concat . map f

join的实施方式可以从分机形式使用等价翻译

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

mu分机形式逆转换由下式给出

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

但一个理论如此抽象为什么要使用任何编程的?

答案很简单:作为计算机科学家,我们的价值抽象!当我们设计的界面的软件组件,我们希望它揭示尽可能少的有关实现的。我们希望能够与许多选择相同的“概念”,很多其他“情况”来代替执行。当我们设计一个通用的接口很多的程序库,它更重要的是,我们选择的接口有多种实现方法。这是我们的价值如此之高,这是因为范畴的理论是如此抽象,它的概念对于编程非常有用的单子概念的普遍性。

这是根本没有suprising,那么,我们下面要介绍的单子的推广也有类理论密切相关。但是,我们强调的是,我们的目的很实际的:它不是“实行分类理论”,它是要找到构建组合子库的一个更一般的方式。这只不过是我们的好运气,数学家已经做了很多的工作我们!

从要概括单子由约翰·休斯箭头


4
投票

这个世界需要的是另一种单子博客文章,但我认为这是在野外识别现有的单子是有用的。

以上是一个叫谢尔宾斯基三角形,我还记得画的唯一分形。分形是自相似的结构和上面的三角形,其中,所述部分与整个(在这种情况下完全规模作为父三角形的一半)。

单子分形。给定一个monadic的数据结构,它的值可以由以形成数据结构的另一个值。这就是为什么它是编程有用的,这就是为什么在很多情况下occurrs。


4
投票

http://code.google.com/p/monad-tutorial/正在进行就是为了解决这个问题的作品。


3
投票

单子是用于封装已改变状态的对象的事情。这是最常见的语言,否则不允许你有修改的状态(例如,哈斯克尔)时发生的。

一个例子是用于文件I / O。

您将能够使用一个单子文件I / O隔离改变状态的性质,只是所使用的单子的代码。单子中的代码可以有效地忽略了世界的变化状态的单子外 - 这使得它更容易来思考你的程序的整体效果。


182
投票

其实,违背了单子的共识,他们什么都没有做的状态。单子是一个简单的方法来包装的东西,并提供方法上做包装的东西操作,而不展开它。

例如,你可以创建一个类来包装一个又一个,在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只能在bindreturn来定义。因此,定义一个单子的时候..你给它的类型(这里是Wrapped a),然后说出它的returnbind操作是如何工作的。

很酷的事情是,这原来是它弹出所有的地方,在一个纯粹的方式封装状态只是其中一个这样的一般模式。

有关单子如何可以用于引入函数依赖,从而控制评价的顺序,就像是在Haskell的IO单子中,检查出IO Inside好文章。

作为理解单子,不用太担心了。阅读他们你觉得有趣,如果你不明白就不要担心。然后,只需在象Haskell语言跳水是要走的路。单子是这些东西的地方理解的实践流淌到你的大脑一个,有一天,你只是突然意识到你了解他们。


3
投票

让下面的“{| a |m}”代表了一块一元数据。该通告的a一种数据类型:

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

功能方面,f,知道如何创建一个单子,只要它有一个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,试图评估一个单子,但被责备。

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

功能,qazxsw POI,找到方法通过使用f提取qazxsw POI。

a

小不>>=知道,单子和 (Muaahaha. How you like me now!?) (Better.) \ | (Give me that a.) (Fine, well ok.) | \ | {| a |m} >>= f 有勾结。

f

但是,他们真的谈什么?嗯,这依赖于单子。仅在抽象的谈论有限制地使用;你必须有特别的单子一些经验来充实理解。

例如,数据Maybe类型

>>=

有一个单子实例,它将行为像下面...

其中,如果案件是 (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

但对于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

在这种情况下,函数知道如何从它的输入列表,但不知道如何处理额外的输入和额外的名单做。绑定f,有助于通过多个输出相结合(Ok, here's your a. Well, its a 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,但它知道一个家伙,将获得IO你。该a单子有a的秘密藏匿,它会通过在桌子底下State st,即使st刚刚要求的f。该f单子类似于a,虽然它只是让Reader rState st

在这一切的一点是,它宣布自己是一个单子的任何类型的数据宣称某种情境的周围提取单子的值。从所有这一切带来的最大益处?那么,它很容易沙发上某种情况下的计算。它会导致混乱,但是,多个上下文载货计算串在一起的时候。所以,程序员不必单子操作照顾解决环境的相互作用。

请注意,使用f的,通过采取一些自主权远离r简化了一个烂摊子。也就是说,在>>=例如上述情况下,f不再到达决定如何在Nothing的情况下做的;它的编码f。这是权衡。如果有必要的Nothing来决定如何在>>=的情况下这样做,那么f应该已经从Nothing一个功能f。在这种情况下,Maybe a作为一个单子是无关紧要的。

但是请注意,有时一个数据类型不导出它的构造函数(看你的IO),如果我们希望与标榜价值的工作,我们别无选择,只能工作与它的单子接口。


168
投票

但是,You could have invented Monads!

SIGFPE说:

但所有这些引进单子的东西需要解释深奥。但我想争辩的是,他们并不深奥的。事实上,面对你会被引导功能编程的各种问题,不可避免地,某些解决方案,所有这些都是单子的例子。事实上,我希望能得到你现在去创造他们,如果你还没有准备好。它是那么一小步注意到,所有这些解决方案实际上都是变相相同的解决方案。而看完这个,你可能会更好地理解单子上的其他文件,因为你会意识到你看,你已经发明的东西应有尽有。

许多该单子试图解决的问题都涉及到的副作用的问题。因此,我们将与他们开始。 (请注意,单子让你这样做不是处理的副作用,特别是许多类型的容器对象可以被看作是单子多。有些介绍给单子很难调和单子的这两种不同的用途,集中力量只有一个或另一个。)

在命令式编程语言,诸如C ++,函数的行为没有像数学函数。例如,假设我们有一个C ++函数,它接受一个浮点参数,并返回浮点结果。从表面上看这似乎有点像一个数学函数映射到实数实数,而是一个C ++函数可以做更多的不仅仅是返回一个数字,取决于它的参数。它可以读取和写入全局变量的值,以及输出写入到屏幕并从用户接收输入。在纯函数式语言,但是,功能只能读取什么是它的参数提供给它,它可以对世界产生影响的唯一途径就是通过它返回的值。


86
投票

单子是有两个操作的数据类型:>>=(又名bind)和return(又名unit)。 return取任意值,并创建了它的单子的一个实例。 >>=取单子的实例和功能上它映射。 (你可以看到已经是一个单子是一种奇怪的数据类型,因为在大多数编程语言,你可以不写一个函数,任意值,并从它创建了一个类型。单子用一种parametric polymorphism的。)

在Haskell符号中,单子接口写入

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

这些操作都应该遵守一定的“规律”,但是这不是很重要了:“法律”只是编纂操作的方式明智的实现应该表现(基本上,>>=return应该同意的价值观是如何被转化成单子实例和>>=是联想)。

单子不只是状态和I / O:他们抽象包括与状态,I / O,异常和不确定性的工作计算的通用模式。大概了解了简单的单子列表和选项类型:

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

其中[]:是列表构造,++是连接运算符,并JustNothingMaybe构造。这两个单子的封装在各自的数据类型的计算的通用和有用的模式(请注意,无论有什么副作用或I / O)。

你真的要玩写了一些不平凡的Haskell代码升值是什么单子约,为什么他们是有用的。


77
投票

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

甲高阶函数是一个简单的函数,它的功能作为一个参数。

函子是任何类型的构造T存在用于其一个高阶函数,调用它map,即变换类型a -> b的函数(给定任何两种类型ab)成一个函数T a -> T b。这map功能也必须服从身份和成分使得下列表达式所有pq(Haskell的符号)返回true规律:

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,和return(有时称为forkpure,或a -> T a)的。对于在Haskell列表:

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

这是为什么有用吗?因为你可以,例如,map一个列表与返回一个列表的功能。 Join需要所产生的名单列表,并连接它们。 List是一个单子,因为这是可能的。

你可以写,做map,然后join功能。该功能被称为bind,或flatMap,或(>>=),或(=<<)。这是一个单子实例通常是如何在Haskell给出。

单子必须满足一定的规律,即join必须是关联的。这意味着,如果你有型x[[[a]]]然后join (join x)应该等于join (map join x)。而pure必须是join这样join (pure x) == x的身份。


45
投票

[免责声明:我仍然在试图完全神交单子。下面正是我到目前为止的理解。如果这是错的,希望有人懂行的会叫我在地毯上。]

鹰写道:

单子是一个简单的方法来包装的东西,并提供方法上做包装的东西操作,而不展开它。

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

  1. 你把某种价值,并与一些附加信息包裹。就像值是某种(例如,整数或字符串),所以附加信息是某一种。 例如,这些额外的信息可能是MaybeIO
  2. 然后,你有一些运营商,让您的包裹数据操作,同时沿附加信息携带。这些运营商使用的附加信息来决定如何改变包装的价值操作的行为。 例如,一个Maybe Int可以是Just IntNothing。现在,如果你添加一个Maybe IntMaybe Int,操作人员将检查,看看他们都是Just Ints内,如果是的话,那里展开Ints,通过他们的加法运算,重新包装所产生的Int到一个新的Just Int(这是一个有效Maybe Int),因此返回Maybe Int。但是,如果他们中的一个是Nothing内,该运营商将只是立即返回Nothing,这又是一个有效的Maybe Int。这样一来,你可以假装你的Maybe Ints只是正常的数字并对其进行定期的数学。如果你要得到一个Nothing,你的公式仍然会产生正确的结果 - 而您不必垃圾检查Nothing无处不在。

但这个例子是Maybe刚刚发生的事情。如果额外信息是个IO,那么,对于IOs定义的特殊操作将改为调用,它可以做一些执行加法之前完全不同。 (OK,把两个IO Ints在一起可能是荒谬的 - 我还没有确定)(另外,如果你关注了Maybe例如,你已经注意到,“包装与额外的东西值”并不总是正确的,但它的。难以准确,正确和精确而不高深莫测的。)

基本上,“单子”大致的意思是“模式”。但是,而不是一本书完全非正式的解释和具体命名的模式的,你现在有一个语言结构 - 语法和所有 - 它允许你在你的程序的东西申报的新模式。 (这里的不精确性是所有的模式都遵循特定的形式,所以单子是不太模式为通用的。但我认为这是大多数人认识和了解最接近的名词。)

这就是为什么人们发现单子如此混乱:因为他们是这样一个通用的概念。要问什么东西是一个单子同样是含糊不清,问什么东西是一种模式。

但认为其在一个模式的这门语言的语法支持的含义:不是有读四本书刚和记忆的特定模式的建设,你只写在一个不可知论者实现这个模式的代码,通用的方式一次,然后你做!然后,您可以重复使用这种模式,如游客或策略或立面也好,都只是装饰用它你的代码的操作,而不必一遍遍重新实现它!

所以这就是为什么谁知道单子的人发现他们非常有用:它不是一些象牙塔的概念,知识势利自豪自己的理解(OK,太当然,teehee),但实际上使代码更简单。


44
投票

多少奋斗之后,我想我终于明白了单子。重读的压倒性投票顶部回答我自己漫长的批评后,我公司会提供这样的解释。

有迹象表明,需要回答理解单子三个问题:

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

正如我在原来的评论中指出,太多的单子的解释得到题号3赶上了,没有了,之前确实充分覆盖问题2,问题1。

为什么你需要一个单子?

象Haskell纯功能的语言是从在如C或Java命令式语言不同,一个纯功能程序不必以特定顺序执行的,一次一个步骤。 Haskell的程序更像是一个数学函数,在其中您可以解决任何数量的潜在订单的“方程式”。这赋予了许多好处,其中之一是,它消除了某些类型的bug,特别是有关像“国家”的可能性。

然而,也有不那么直接与这种风格的编程来解决某些问题。有些东西,比如控制台程序和文件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%!");

这的确是写这更方便的方式。我们怎么做,虽然?

什么是单子?

一旦你有一个类型(如consolestate),您有一堆专门设计用于该类型操作的函数限定沿,则可以通过定义像:(绑定)的操作者打开这些东西整个包装成“单子”该自动输送其留在其权限返回值,为函数的参数,以及lift运营商,轮流正常功能,成与特定类型的绑定运营商的工作职能。

单子是如何实现的?

见其他的答案,似乎很随意跳入个中详情。


35
投票

(参见在What is a monad?答案)

一个好的动机单子是SIGFPE(丹Piponi)的You Could Have Invented Monads! (And Maybe You Already Have)。有a LOT of other monad tutorials,其中许多misguidedly尝试在“深入浅出”利用各种类比来解释单子:这是monad tutorial fallacy;避免它们。

由于DR麦基弗说,在Tell us why your language sucks

所以,事情我恨哈斯克尔:

让我们先从明显。 Monad的教程。不,不是单子。具体教程。他们是无止境的,夸大和亲爱的上帝是他们乏味。此外,我从来没有见过任何令人信服的证据表明他们实际的帮助。阅读类的定义,编写一些代码,克服了可怕的名字。

你说你明白也许单子?好,你对你的方式。刚开始使用其他的单子,迟早你会明白是什么一般的单子。

[如果你是数学为主,你可能想忽略几十个教程和学习的定义,或者按照lectures in category theory :)定义的主要部分是一个单子中号涉及“型构造”定义为每个现有的类型“ T”的新型‘MT’,并来回之间某些方面的‘常规’型和‘M’型。]

此外,令人惊讶的是,最好的引见一个单子实际上是早期的学术论文引入单子,菲利普·沃德勒的Monads for functional programming之一。它实际上具有实用,不平凡的激励例子,与许多人工教程那里的。

© www.soinside.com 2019 - 2024. All rights reserved.