什么是单子?


1414

最近对Haskell进行了简要介绍后,关于monad本质上是什么的简要而简洁的实际解释是什么?

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


12
埃里克·利珀特(Eric Lippert)为此问题写了一个答案(stackoverflow.com/questions/2704652/…),这是由于某些问题存在于单独的页面中。
P Shved 2010年

70
这是使用javascript的新介绍-我发现它非常易读。
Benjol 2011年


20
另请参见图片中的
Monad

2
monad是具有辅助操作的一系列功能。看到这个答案
cibercitizen1 2014年

Answers:


1059

首先:如果您不是数学家,那么monad一词会有些空洞。一个替代术语是计算构建器,它更能说明它们的实际用途。

您要求提供实际示例:

示例1:列表理解

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

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

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

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 ++ "!")

两个示例都使用monad,即AKA计算生成器。共同的主题是monad 以某些特定的有用方式链接操作。在列表理解中,操作链接在一起,这样,如果操作返回列表,则对列表中的每个项目执行以下操作。另一方面,IO monad顺序执行操作,但传递一个“隐藏变量”,代表“世界的状态”,这使我们能够以纯功能的方式编写I / O代码。

事实证明,链接操作的模式非常有用,可用于Haskell中的许多不同事物。

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

列表理解语法和do表示法都是使用>>=操作符进行链接操作的语法糖。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#也支持monad。这个例子是从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它实际上在一个单独的线程上等待响应,而主线程从该函数返回。收到响应后,在生成的线程上执行最后三行。

在大多数其他语言中,您必须为处理响应的行显式创建一个单独的函数。该asyncmonad可以自己“拆分”该块并推迟执行后半部分。(async {}语法表明该块中的控制流由asyncmonad 定义。)

他们如何工作

那么,monad如何做所有这些花哨的控制流程呢?do-block(或在F#中称为计算表达式)实际发生的事情是,每个操作(基本上每行)都包装在一个单独的匿名函数中。然后使用bind运算符(>>=在Haskell中拼写)组合这些功能。由于该bind操作合并了函数,因此可以按自己的意愿执行它们:依次,多次,相反地,丢弃某些函数,并在需要时在单独的线程上执行某些函数,依此类推。

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

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

这很丑陋,但实际发生的情况也更明显。该>>=操作是神奇的成分:它需要一个值(左侧)和联合其与功能(右侧),以产生新的价值。然后,新值将由下一个>>=运算符获取,并再次与函数结合以产生新值。>>=可以看作是迷你评估器。

请注意,这>>=是针对不同类型的重载,因此每个monad都有其自己的实现>>=。(尽管链中的所有操作都必须属于同一单子类型,否则>>=运算符将无法工作。)

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

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

加起来

在Haskell术语中,monad是参数化类型,它是Monad类型类的实例,该类>>=与其他一些运算符一起定义。用外行术语来说,单子仅是为其>>=定义操作的类型。

它本身>>=只是一种繁琐的函数链接方式,但是由于存在隐藏“管道”的do-notation,所以monadic操作被证明是非常好的和有用的抽象,在语言中的很多地方都有用,并且有用用于创建自己的语言的迷你语言。

为什么单子很难?

对于许多Haskell学习者而言,单子电池就像壁垒一样是一个障碍。并不是说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


65
作为一个在理解单子方面有很多问题的人,我可以说这个答案有所帮助。但是,还有一些我不理解的事情。单子列表理解是什么?该示例是否有扩展形式?另一个让我对大多数monad解释(包括其中一个)感到不安的另一件事是-他们是否一直在混淆“什么是monad?与“ monad有什么用?” 和“如何实施monad?”。当您写到“ monad基本上只是支持>> =运算符的类型”时,您就为之震惊。哪有我...
布列塔尼

82
我也不同意你关于为什么单子很难的结论。如果单子电池本身并不复杂,那么您应该能够一堆行李就能解释它们的含义。当我问“什么是单子”这个问题时,我不想了解实现,我想知道这意味着什么。到目前为止,似乎答案是“因为haskell的作者是施虐受虐狂,他们决定您应该做一些愚蠢的事情来完成简单的事情,因此您必须学习monad以使用haskell,而不是因为它们在某种程度上对自己” ...
布列塔尼

69
但是..那不可能是正确的,是吗?我认为monad很难,因为似乎没人能弄清楚如何解释它们,而不会陷入混乱的实现细节中。我的意思是..什么是校车?这是一个金属平台,其前端带有一个装置,该装置消耗精炼石油产品来循环驱动一些金属活塞,这些活塞进而旋转连接到某些齿轮的曲轴,这些齿轮驱动一些车轮。车轮周围有膨胀的橡胶袋,这些橡胶袋与沥青表面接触,使一组座椅向前移动。座位向前移动是因为……
布列塔尼

130
我阅读了所有这些内容,除了Haskell程序员对解释的理解还不够,所以仍然不知道monad是什么。这些示例并没有多大帮助,因为这些都是没有monad便可以做的所有事情,而且这个答案也无法使monad使它们变得更容易,只会更令人困惑。这个答案的一个非常有用的部分是示例2的句法糖被删除了。我之所以说接近,是因为除了第一行之外,扩展与原始扩展没有任何实际相似之处。
Laurence Gonsalves

81
似乎是monad的解释所特有的另一个问题是它是用Haskell编写的。我并不是说Haskell是一门糟糕的语言-我是说这对解释Monad是一门糟糕的语言。如果我知道Haskell我已经懂单子词了,那么如果您想解释单子词,请先使用一种不知道单子词的人更容易理解的语言。如果必须使用Haskell,则完全不要使用语法糖-使用您可以使用的最小,最简单的语言子集,并且不要假设您对Haskell IO有所了解。
劳伦斯·贡萨尔维斯

712

解释“什么是单子”有点像说“什么是数字?” 我们一直在使用数字。但是想象一下您遇到了一个对数字一无所知的人。如何赫克你能解释的数字是什么?您甚至将如何描述它可能有用的原因?

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

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

因此,bind函数就像一个分号;它分离了流程中的步骤。绑定函数的工作是获取上一步的输出,并将其输入到下一步。

听起来不太难,对吧?但是不止一种monad。为什么?怎么样?

好了,bind函数可以只从一个步骤中获取结果,然后将其提供给下一步。但是,如果那是“全部”,那单子弹就可以了……那实际上不是很有用。理解这一点很重要:每个有用的 monad 除了成为monad 之外,还会执行其他操作。每个有用的单子都具有“特殊能力”,这使其具有独特性。

(一个没有任何特殊要求的monad称为“ identity monad”。这与identity函数一样,听起来似乎毫无意义,但事实并非如此……但这是另一个故事™。)

基本上,每个monad都有其自己的bind函数实现。而且,您可以编写一个绑定函数,使其在执行步骤之间做一些令人讨厌的事情。例如:

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

  • 扩展这个想法,您可以实现“异常”。(Error MonadException Monad。)由于您是自己定义它们,而不是作为一种语言功能,因此可以定义它们的工作方式。(例如,也许您想忽略前两个异常,而仅在抛出第三个异常时才中止。)

  • 您可以使每个步骤返回多个结果,并使bind函数遍历它们,将每个结果反馈给您。这样,在处理多个结果时,您不必一直在各处编写循环。绑定功能“自动”为您完成所有操作。(列表Monad。)

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

  • 您可以这样做,以便替换“额外数据”。这使您可以模拟破坏性更新,而无需实际进行破坏性更新。(国家Monad及其堂兄作家Monad。)

  • 因为您仅模拟破坏性更新,所以您可以琐碎地执行真正的破坏性更新无法完成的任务。例如,您可以撤消上一次更新,或恢复为旧版本

  • 您可以将monad放在可以暂停计算的位置,这样您可以暂停程序,进入并修改内部状态数据,然后继续执行。

  • 您可以将“ continuations”实现为monad。这可以让您放心!

使用monad可以实现所有这些以及更多功能。当然,如果没有 monad ,所有这些都是完全可能的。这只是大幅容易使用的单子。


13
我感谢您的回答-特别是最后的让步,所有这些当然也可以在没有monad的情况下实现。需要指出的一点是,使用monad 容易,但是效率通常不如不使用monad那样高效。一旦需要使用转换器,功能调用(和创建的功能对象)的额外分层将难以看清和控制,并且由于巧妙的语法而变得不可见。
seh 2012年

1
至少在Haskell中,优化器消除了monad的大部分开销。因此,唯一真正的“成本”是所需的脑力。(如果您关心“可维护性”,这并不重要。)但是通常,单子使事情变得简单而不是困难。(否则,您为什么要打扰?)
MathematicalOrchid

我不确定Haskell是否支持此功能,但是从数学上讲,您可以使用>> =来定义monad并返回,join和ap。>> =和return是使monad实际有用的原因,但是join和ap对monad是什么有更直观的了解。
Jeremy List

15
来自非数学,非功能性编程背景,这个答案对我来说最有意义。
jrahhali

10
这是第一个答案,实际上使我对单子到底是什么感到有些了解。感谢您找到一种解释方式!
robotmay17年

186

实际上,与对Monad的普遍理解相反,它们与国家无关。Monad只是包装事物的一种方法,并且提供了对包装的事物进行操作而无需对其进行包装的方法。

例如,您可以在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。因此,当定义一个monad ..时,请给出其类型(此处为Wrapped a),然后说出其returnbind运算的工作方式。

有趣的是,这是一种通用的模式,它突然出现在整个地方,以纯净的方式封装状态只是其中之一。

要获得一篇有关如何使用monad引入功能依赖关系并从而控制求值顺序的好文章,就像在Haskell IO monad中使用的那样,请查看 IO Inside

至于了解单子,不要太担心。了解他们的有趣之处,如果您不立即了解,也不必担心。然后,仅使用Haskell这样的语言进行潜水就可以了。Monad是其中的一种,通过练习,理解会渗入大脑,有一天,您突然意识到自己已经理解它们。


->是右关联的镜像函数应用程序,它是左关联的,因此省略括号在这里没有什么区别。
Matthias Benkard

1
我认为这根本不是一个很好的解释。Monad简直是一种方式?好的,哪条路?为什么我不使用类而不是monad进行封装?
布列塔尼

4
@ mb21:如果您只是指出括号太多,请注意,a-> b-> c实际上仅是a->(b-> c)的缩写。将此特定示例写为(a-> b)->(Ta-> Tb)严格来说只是添加不必要的字符,但是从道德上讲,这是“正确的做法”,因为它强调fmap映射a->类型的函数b转换为Ta-> Tb类型的函数。最初,这是函子在范畴论中所做的,也是单子的来源。
Nikolaj-K 2014年

1
这个答案是误导的。有些monad根本没有“包装器”,例如具有固定值的功能。

1
@DanMandel Monads是提供其自己的数据类型包装器的设计模式。Monads以抽象样板代码的方式设计。因此,当您在代码中调用Monad时,它会在后台执行您不需要担心的事情。考虑一下Nullable <T>或IEnumerable <T>,它们在幕后做什么?那就是Monad。
sksallaj

168

但是,您可能已经发明了Monads!

sigfpe说:

但是所有这些都将monads引入作为需要解释的深奥事物。但是我想说的是,它们根本不是深奥的。实际上,面对函数式编程中的各种问题,您不可避免地会遇到某些解决方案,所有这些都是monad的示例。实际上,我希望您现在就发明它们。然后,迈出一小步即可注意到所有这些解决方案实际上都是变相的相同解决方案。阅读完此书后,您可能会更好地了解monad上的其他文档,因为您将认识到所有已被发明的东西。

Monad尝试解决的许多问题都与副作用有关。因此,我们将从它们开始。(请注意,单子命令不仅可以处理副作用,而且可以将许多类型的容器对象视为单子命令。对单子命令的一些介绍发现,很难兼顾这两种不同的单子用法,而仅专注于一个或多个。另一个。)

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


9
…最好的方式不仅在互联网上,而且在任何地方。(我在下面的回答中提到的Wadler的原始论文《用于函数式编程的Monads》也不错。)千篇一律的模拟教程都不是一件容易的事
ShreevatsaR 2011年

13
Sigfpe帖子的JavaScript翻译是学习monad的最佳新方法,对于尚未熟悉高级Haskell的人来说!
山姆·沃特金斯

1
这就是我了解单子的方法。引导读者完成发明概念的过程通常是教授该概念的最佳方法。
乔丹

但是,接受屏幕对象作为参数并返回其修改后的文本的副本的函数将是纯函数。
Dmitri Zaitsev

87

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

用Haskell表示法编写monad接口

class Monad m where
  return :: 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 where
    Just x  >>= k = k x
    Nothing >>= k = Nothing
    return x      = Just x

其中[]:是列表构造函数,++是串联运算符,Just并且NothingMaybe构造函数。这两个单子都在它们各自的数据类型上封装了通用且有用的计算模式(请注意,两者均与副作用或I / O没有任何关系)。

您确实必须尝试编写一些平凡的Haskell代码,以了解monad的含义以及它们为何有用。


您将功能映射到它到底是什么意思?
Casebash

Casebash,在介绍中我故意是非正式的。请参阅最后的示例,以了解“映射功能”的含义。
克里斯·康威

3
Monad不是数据类型。这是组成函数的规则:stackoverflow.com/a/37345315/1614973
Dmitri Zaitsev

@DmitriZaitsev是正确的,Monads实际上提供了自己的数据数据类型,Monads arent数据类型
sksallaj

78

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

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

算符是任何类型的构造T为其中存在一个高阶函数,调用它map,该变换类型的函数a -> b(给定任何两种类型的ab)成一个函数T a -> T b。此map函数还必须遵守身份和组成定律,以使以下表达式对all pq(Haskell表示法)返回true :

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

例如,如果一个类型构造函数被称为Listfunctor,则它具有(a -> b) -> List a -> List b遵循上述规则的类型函数。唯一实际的实现是显而易见的。结果List a -> List b函数遍历给定列表,(a -> b)为每个元素调用该函数,然后返回结果列表。

一个单子本质上只是一个仿函数T有两个额外的方法,join,类型T (T a) -> T a,和unit(有时被称为returnforkpure类型)a -> T a。对于Haskell中的列表:

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

为什么这样有用?例如,因为您可以map使用返回列表的函数来覆盖列表。Join获取列表的结果列表并将其连接起来。List是monad,因为这是可能的。

你可以写,做了功能map,那么join。此函数称为bindflatMap(>>=)或或(=<<)。通常,这是在Haskell中给出monad实例的方式。

一个单子必须满足某些法律,即join必须具有关联性。这意味着,如果你有一个价值x型的[[[a]]]join (join x)应该等于join (map join x)。而且pure必须是一个身份join,使得join (pure x) == x


3
“高阶函数”的定义略有增加:它们可以使用OR RETURN函数。这就是为什么他们是“更高”的角色,因为他们自己做事。
凯文(Kevin)

9
根据该定义,加法是高阶函数。它需要一个数字,并返回一个将该数字添加到另一个函数。因此,不,高阶函数是严格的函数,其域由函数组成。
Apocalisp 2010年

视频“ Brian Beckman:别担心Monad ”遵循同样的逻辑。
icc97

48

[免责声明:我仍在尝试完全使用monad。以下是到目前为止我所了解的。如果错了,希望有知识的人会在地毯上给我打电话。]

阿尔纳尔写道:

Monad只是包装事物的一种方法,并且提供了对包装的事物进行操作而无需对其进行包装的方法。

就是这样。这个想法是这样的:

  1. 您需要某种价值,并在其中附加一些其他信息。就像值是某种类型(例如整数或字符串)一样,附加信息也是某种类型。

    例如,多余的信息可能是MaybeIO

  2. 然后,您将拥有一些运算符,使您可以在打包的数据上进行附加信息的操作。这些运算符使用附加信息来决定如何更改包装值上的操作行为。

    例如,a Maybe Int可以是a Just IntNothing。现在,如果你添加一个Maybe IntMaybe Int,运营商将检查,看看他们都是Just Int里面装的,如果是的话,那里展开IntS,通过他们的加法运算,形成的再包装Int到一个新的Just Int(这是一个有效Maybe Int),然后返回Maybe Int。但是,如果其中一个是Nothing内部,则此运算符将立即返回Nothing,这又是有效的Maybe Int。这样,您可以假装Maybe Ints只是普通数字,并对它们执行常规数学运算。如果要获得a Nothing,您的方程式仍将产生正确的结果- 无需在Nothing任何地方乱扔检查

但是这个例子正是发生了什么Maybe。如果额外的信息是IO,则将IO调用为s 定义的特殊运算符,它可以在执行加法之前做一些完全不同的事情。(好吧,将两个IO Ints加在一起可能是无意义的-我不确定。)(此外,如果您关注Maybe示例,您会注意到“用额外的东西包装价值”并不总是正确的。但这很难准确,正确和精确而又不言而喻。)

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

这就是为什么人们会觉得单子如此令人困惑:因为它们是一个通用的概念。问什么使某物成为单子与问什么使某物成为一种模式相似。

但是,请考虑一下在语言中对模式的想法提供语法支持的含义:不必阅读《四人帮》一书并记住特定模式的构造,只需编写以不可知论的方式实现该模式的代码,一般的方法一次就完成了!然后,您可以重复使用这种模式,例如Visitor或Strategy或Façade,等等,只需用它装饰代码中的操作即可,而不必一遍又一遍地重新实现!

这就是为什么了解 monad的人会发现它们如此有用的原因:知识分子势利的人以理解为荣并不是象牙塔的概念(当然,当然也是如此),但是实际上使代码更简单。


12
有时,来自“学习者”(如您)的解释比来自专家的解释与另一个学习者更相关。学习者的想法相同:)
Adrian

使某物成为monad的原因是存在type函数M (M a) -> M a。您可以将其转换为一种类型的事实M a -> (a -> M b) -> M b是使它们有用的原因。
杰里米·

“ monad”大致表示“模式”。
谢谢您

44

经过多方努力,我认为我终于了解了单子。在重新阅读了我对压倒性多数投票的冗长评论之后,我将提供这种解释。

要了解单子,需要回答三个问题:

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

正如我在原始评论中指出的那样,太多的单子论解释陷入了第3个问题,而没有真正涵盖第2个问题或第一个问题。

为什么需要单子?

像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%!");

实际上,这将是一种更方便的编写方式。但是我们该怎么做呢?

什么是单子?

一旦consolestate定义了一个类型(例如)以及专门设计用于对该类型进行操作的一堆函数,就可以通过定义一个:自动(如(绑定))运算符将这些东西的整个包变成“ monad” 将左侧的返回值输入右侧的函数参数,并将lift正常函数转换为与该特定种类的绑定运算符一起使用的函数的运算符。

monad如何实现?

看到其他答案,似乎很容易进入其中。


排序并不是定义monad的唯一原因。一个monad就是具有绑定和返回的任何函子。绑定并返回给您排序。但是他们也提供其他东西。另外,请注意,您最喜欢的命令式语言实际上是带有OO类的高级IO monad。轻松定义monad意味着使用解释器模式很容易-将dsl定义为monad并对其进行解释!
2014年


38

几年前给出了这个问题的答案后,我相信我可以通过...来改善和简化该回答。

monad是一种功能组合技术,它使用组合功能外部化某些输入方案的处理bind,以在组合期间对输入进行预处理。

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

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

可以通过重组输入来改进此设计,以便更轻松地查询相关状态。因此,例如,如果包括有效性概念,则y值可能会变得不那么简单。Mb(is_OK, b)y

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

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

同样,这里的组合自然发生了compose,因此每个函数必须分别处理其输入的所有情况,尽管这样做现在要容易得多。

但是,如果我们可以将处理情景的常规时间的审讯工作外化,该怎么办。例如,如果输入不正确(如when is_OKis)时程序不执行任何操作,该怎么办false?如果这样做了,则组合函数将不需要自己处理该场景,从而大大简化了它们的代码并实现了另一级重用。

为了实现这种外在化,我们可以使用函数bind (>>=)composition代替compose。因此,与其简单地将值从一个函数的输出传递到另一个函数的输入,Bind不如检查其M一部分Ma并决定是否以及如何将组合函数应用于a。当然,该功能bind将针对我们的特定情况进行定义,M以便能够检查其结构并执行我们想要的任何类型的应用程序。尽管如此,因为a可以bind仅通过的,所以可以是任何东西a在确定应用程序必要时才检查的给组合函数。此外,组合函数本身不再需要处理M输入结构的一部分,以简化它们。因此...

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

简而言之,一旦输入被设计为足以暴露它们,那么monad就会外部化,从而围绕某些输入方案提供标准行为。此设计是一个shell and content模型,其中外壳包含与组合功能的应用程序相关的数据,并由该bind功能询问并且仅对功能可用。

因此,单子是三件事:

  1. M用于存放monad相关信息的外壳,
  2. 一个bind实现该功能的函数,用于在将组合函数应用于它在外壳中找到的内容值时使用此外壳信息,以及
  3. 形式的可组合函数a -> Mb,产生包含单子管理数据的结果。

一般而言,函数的输入比其输出的约束要严格得多,输出可能包括错误条件。因此,Mb结果结构通常非常有用。例如,除数为时,除法运算符不返回数字0

此外,monads可能包括包装函数,这些包装函数通过在应用后将其结果包装起来,从而将值包裹a到monadic类型中Ma,并将通用函数包裹a -> b到monadic函数中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以上,力括号内的早期评估bindingf,并g只会导致一个功能,预计Ma为了完成bind。因此,Ma必须先确定的评估,然后才能将其值应用到,f并将结果应用到g


“ ...但我希望其他人觉得它有用”,尽管强调了所有句子,但它对我来说确实是有用的:D

这是我读过/看过/听过的有关monad的最简洁明了的解释。谢谢!
詹姆斯

Monad和Monoid之间有重要的区别。单子是间“撰写”功能的规则不同类型的,这样他们就不会形成一个二进制的操作需要幺,在这里看到更多的细节:stackoverflow.com/questions/2704652/...
德米特里·扎伊采夫

是。你是对的。你的文章让我头疼:)。但是,我发现这种治疗非常有帮助(并将其添加到我的指导中)。感谢您的注意:stackoverflow.com/a/7829607/1612190
George

2
您可能已经将代数群论与Monad来自的范畴论混淆了。前者是代数群论,与之无关。
Dmitri Zaitsev'3

37

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

在Visual Basic / C#中,“也许单子”本质上等效于“可空类型”。它采用非空类型“ T”,并将其转换为“ Nullable <T>”,然后定义所有二进制运算符对Nullable <T>的含义。

副作用被相似地表示。将创建一个结构,其中包含副作用的描述以及函数的返回值。然后,在函数之间传递值时,“提升”操作会在副作用周围进行复制。

它们被称为“ monads”,而不是“类型运算符”更易于掌握的名称,原因如下:

  1. Monad对其功能有限制(有关详细信息,请参见定义)。
  2. 这些限制以及涉及三个运算的事实符合类别理论中称为monad的事物的结构,monad是数学的一个模糊分支。
  3. 它们是由“纯”功能语言的支持者设计的
  4. 纯函数语言的支持者,例如模糊的数学分支
  5. 由于数学晦涩难懂,并且monad与特定的编程风格相关联,因此人们倾向于将monad用作一种秘密握手。因此,没有人愿意花钱买一个更好的名字。

1
Monad不是“设计”的,而是从一个领域(类别理论)应用于另一个领域(纯功能编程语言的I / O)。牛顿是否“设计”了微积分?
Jared Updike

1
上面的第1点和第2点是正确且有用的。第4点和第5点是一种自发性,即使或多或少是正确的。他们并没有真正帮助解释单子。
Jared Updike

13
回复:4、5:“秘密握手”是一条红鲱鱼。编程充满了行话。Haskell恰好称其为“东西”而不假装重新发现某些东西。如果它已经存在于数学中,为什么还要为其重新命名?这个名字确实不是人们没有获得单子的原因。他们是一个微妙的概念。一般人可能理解加法和乘法,为什么他们不理解阿贝尔群的概念呢?因为它更抽象,更笼统,而且该人还没有完成将想法包裹起来的工作。名称更改无济于事。
Jared Updike

16
igh ...我不是要攻击Haskell ...我只是在开玩笑。因此,我并不太想成为“ ad hominem”。是的,演算是“设计的”。例如,这就是为什么微积分学生被教为莱布尼兹(Leibniz)表示法,而不是Netwton所使用的讨厌的东西。更好的设计。好名字可以帮助您理解很多东西。如果我将Abelian Groups称为“张大的皱纹豆荚”,您可能很难理解我。您可能会说“但这个名字是胡说八道”,没人会这样称呼他们。对于从未听说过类别理论的人来说,“单子”听起来像胡说八道。
Scott Wisniewski

4
@斯科特:对不起,如果我的大量评论使我似乎对哈斯克尔感到防御。我喜欢您对秘密握手的幽默,并且您会注意到我说的或多或少是真实的。:-)如果您将Abelian Groups称为“大皱纹豆荚”,您将尝试给monads一个“更好的名字”(参见F#“ computation expressions”)会犯同样的错误:该词存在并且关心的人知道monads是什么是,但不是“温暖的模糊事物”(或“计算表达式”)。如果我正确理解您对术语“类型运算符”的使用,除了monad之外,还有很多其他类型的运算符。
Jared Updike

35

(另请参阅“ 什么是单声道?”中的答案

sigfpe(Dan Piponi)的《您可能已经发明了Monads》是Monads的一个很好动机(也许您已经拥有了)。还有很多其他的monad教程,其中许多使用各种类比方法以错误的方式试图用“简单的术语”来解释monad:这是monad的谬论;避免他们。

正如MacIver博士在告诉我们您的语言为何糟透了

因此,我讨厌Haskell:

让我们从显而易见的地方开始。Monad教程。不,不是单子。特别是教程。他们是无尽的,夸张的,亲爱的上帝,他们乏味。此外,我从未见过任何令人信服的证据证明它们确实有所帮助。阅读类定义,编写一些代码,克服可怕的名字。

您说您了解Maybe monad吗?好,您在路上。刚开始使用其他monad,迟早您将了解一般的monad。

[如果您以数学为导向,则可能要忽略数十个教程并学习其定义,或者按照类别理论进行讲授:)定义的主要部分是Monad M涉及为每个对象定义的“类型构造函数”现有类型“ T”和新类型“ MT”,以及在“常规”类型和“ M”类型之间往返的一些方法。

同样,令人惊讶的是,对monad的最佳介绍之一实际上是介绍monad的早期学术论文之一,即Philip Wadler的函数编程Monad。实际上,它有一些实用的,不平凡的激励性示例,与现有的许多人工教程不同。


2
Wadler论文的唯一问题是表达方式不同,但我同意该论文非常引人注目,并且是应用monad的简洁明了的动机。
Jared Updike

为“ monad教程谬论” +1。关于monad的教程类似于提供一些试图解释整数概念的教程。一个教程会说:“ 1类似于一个苹果”。另一个教程说:“ 2就像梨子”;第三个说:“ 3基本上是橙色”。但是,您永远都不会从任何单个教程中获得全部内容。我从中得出的结论是单子是一个抽象概念,可以用于许多完全不同的目的。
stakx-在2011年

@stakx:是的,是的。但是我并不是说单子词是您不能学习或不应该学习的抽象;只有在您看到足够多的具体示例以感知单个基础抽象之后,最好学习它。在这里查看我的其他答案
ShreevatsaR 2011年

5
有时,我觉得有太多教程试图通过使用执行复杂或有用内容的代码来使读者相信单子语法是有用的。那阻碍了我几个月的理解。我不是那样学习的。我更喜欢看到非常简单的代码,做一些愚蠢的事情,我可以从头脑中审阅,但是找不到这种示例。我无法了解第一个示例是否是解析复杂语法的monad。我可以了解它是否是求和整数的单子。
拉斐尔·卡尔萨维里尼

仅提及类型构造函数是不完整的: stackoverflow.com/a/37345315/1614973
Dmitri Zaitsev

23

Monad将控制流的抽象数据类型是数据。

换句话说,许多开发人员对集合,列表,字典(或哈希或地图)和树的想法感到满意。在这些数据类型中,有许多特殊情况(例如InsertionOrderPreservingIdentityHashMap)。

但是,面对程序“流程”时,许多开发人员没有比if,switch / case,do,while,goto(grr)和(也许)闭包更多的结构。

因此,monad仅仅是控制流的构造。代替monad的更好的说法是“控件类型”。

这样,monad具有用于控制逻辑,语句或函数的插槽-数据结构中的等效项是表示某些数据结构允许您添加和删除数据。

例如,“ if”单子:

if( clause ) then block

最简单的有两个插槽-一个子句和一个块。该if单子通常是建立评估条款的结果,如果不是假的,评估该块。许多开发人员在学习“ if”时并没有被介绍给monad,因此不必理解monad来编写有效的逻辑。

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

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

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


14

我最喜欢的Monad教程:

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

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

@Stu:monad的要点是允许您向通常为纯代码的地方添加(通常)顺序语义;您甚至可以编写monad(使用Monad Transformers)并获得更有趣和更复杂的组合语义,例如使用错误处理,共享状态和日志记录进行解析。所有这一切都可以用纯代码实现,monads仅允许您将其抽象出来并在模块化库中重用(总是擅长编程),并提供方便的语法使其显得势在必行。

Haskell已经有运算符重载[1]:它使用类型类的方式与人们可能在Java或C#中使用接口的方式很相似,但是Haskell恰好也允许非字母数字标记(如+ &&和>)用作中缀标识符。如果您的意思是“重载分号” [2],那么这只是运算符在您查看方式中的重载。听起来像是黑魔法,并要求麻烦“使分号超载”(画面敏锐的Perl黑客对此想法颇有兴趣),但要点是,没有monads就不会有分号,因为纯功能代码不需要或不允许显式排序。

这听起来比需要的要复杂得多。sigfpe的文章非常酷,但是使用Haskell进行了解释,这无法打破理解Haskell吞噬Monads和理解Monads吞噬Haskell的鸡蛋问题。

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

[2]这也是一个过分的简化,因为用于链接单子动作的运算符是>> =(发音为“ bind”),但是有语法糖(“ do”)可以让您使用花括号和分号和/或缩进和换行符。


9

最近,我一直以不同的方式想到Monads。我一直认为它们是抽象出执行顺序数学方式,这使得新型的多态性成为可能。

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

在简单的情况下,当您使用monad时,感觉就一样-您定义了顺序出现的表达式列表。除此之外,根据您使用的monad,您的代码可能会按顺序运行(例如在IO monad中),一次并行运行在多个项目上(例如在List monad中),它可能会中途停止运行(例如在Maybe monad中) ,它可能会中途暂停,以稍后再恢复(例如在Resumetion单子中),它可能倒退并从头开始(例如在Transaction单子中),或者可能后退以尝试其他选项(例如在Logic单子中) 。

而且由于monad是多态的,因此可以根据需要在不同的monad中运行相同的代码。

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


9

我对monad还是陌生的,但我想我应该分享一个链接,我觉得阅读起来真的很棒(有图片!!):http : //www.matusiak.eu/numerodix/blog/2012/3/11/外行monads / (无从属关系)

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

我希望我做对了,更重要的是,希望本文对monads有一个正确的看法。如果没有别的,这篇文章有助于激发我的兴趣,以了解更多有关单子的知识。


python示例使您容易理解!感谢分享。
瑞安·埃芬迪

8

除了上述出色的答案外,让我为您提供以下文章的链接(由Patrick Thomson撰写),该文章通过将概念与JavaScript库jQuery(及其使用“方法链”操作DOM的方式)相关联来说明单子。: jQuery是Monad

jQuery的文档本身并不是指术语“单子”,但谈到了“构建者模式”,这可能是比较熟悉的。这并不会改变您那里可能有一个适当的monad的事实,甚至没有意识到。


如果您使用jQuery,则这种解释可能会非常有帮助,尤其是当您的Haskell不强时
byteclub 2010年

10
jQuery显然不是monad。链接的文章是错误的。
托尼·莫里斯

1
“强调”并不是很令人信服。有关该主题的一些有用讨论,请参阅jQuery是monad
吗?-Stack

1
另请参阅道格拉斯·克拉克福德(Douglas Crackford)的Google Talk Monads和Gonads及其用于执行modads的Javascript代码,扩展了AJAX库和Promises的类似行为:douglascrockford / monad·GitHub
nealmcb 2013年


7

monad是一种将具有共同上下文的计算组合在一起的方法。就像建立管道网络一样。构建网络时,没有数据流过。但是,当我完成所有位与“绑定”和“返回”的拼写之后,我将调用类似的东西runMyMonad monad data,数据将通过管道流动。


1
那比Monad更像Applicative。使用Monads,必须先从管道中获取数据,然后才能选择要连接的下一个管道。
Peaker

是的,您描述的是适用产品,而不是Monad。Monad正在根据管道内到达该点的数据在现场构建下一个管道段。
威尔·内斯



5

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

格雷厄姆·赫顿(Graham Hutton)在《 Haskell中编程》一书中的第8章“功能解析器”。实际上,这根本没有提到monad,但是,如果您可以通读本章并真正理解其中的所有内容,尤其是如何评估绑定操作序列,那么您将了解monad的内部。期望这需要几次尝试。

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


5

Monoid似乎可以确保在Monoid上定义的所有操作以及受支持的类型始终在Monoid内返回受支持的类型。例如,任何数字+任何数字=一个数字,没有错误。

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

无论如何,似乎Monads只是确保操作链以可预测的方式运行的一种方法,而声称为Num-> Num的函数由Num-> Num的另一个函数x组成,例如,发射导弹。

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

在Haskell中,main类型是IO()或IO [()],该区域很奇怪,我将不讨论它,但是我认为这是发生的情况:

如果我有main,则希望它执行一系列操作,运行该程序的原因是产生效果-通常是通过IO。因此,我可以将IO操作主要连接在一起,以便进行IO,而无需做其他事情。

如果我尝试执行不“返回IO”的操作,程序将抱怨链条不畅通,或者基本上是“这与我们要执行的操作有什么关系-IO操作”,它似乎很强制程序员可以保持思路,而不必逃避和思考发射导弹,而可以创建排序算法-这种算法不会流动。

基本上,Monads似乎是编译器的一个提示,“嘿,您知道此函数在此处返回一个数字,它实际上并不总是起作用,它有时可以产生一个Number,有时什么也没有,只要将其保留在心神”。知道这一点后,如果您尝试声明一个monadic动作,则该monadic动作可能会充当编译时异常,说明“嘿,这实际上不是数字,这可以是数字,但是您不能假设这样做,可以做点什么以确保流量可以接受。” 这可以在一定程度上防止程序行为无法预测。

似乎单子论与纯度无关,也不与控制无关,而是与保持类别的标识有关,在该类别上所有行为都是可以预测和定义的,或者不能编译。当您期望做某事时,您什么也不会做;如果您什么都不做(可见),则您将无法做某事。

我对Monads想到的最大原因是-查看过程/ OOP代码,您会发现您不知道程序从哪里开始,也没有结束,您看到的只是很多跳跃和很多数学运算,魔术和导弹。您将无法维护它,并且,如果可以的话,您将花费大量时间在整个程序上全神贯注,然后才可以理解它的任何部分,因为在这种情况下,模块化是基于相互依赖的“节”代码,其中将代码优化为尽可能相关,以保证效率/相互关系。Monad非常具体,并通过定义进行了很好的定义,并确保程序流能够分析和隔离难以分析的部分,因为它们本身就是Monad。单子似乎是“ 甚至毁灭宇宙甚至扭曲时间-我们都不知道,也没有任何保证。monad保证它就是它。这非常强大。甚至毁灭宇宙甚至扭曲时间-我们都不知道,也没有任何保证。monad保证它就是它。这非常强大。

从某种意义上说,“现实世界”中的所有事物似乎都是单子形式的,因为它受到防止混淆的明确可观察定律的约束。这并不意味着我们必须模仿该对象的所有操作来创建类,而是可以简单地说“一个正方形就是一个正方形”,只不过是一个正方形,甚至不是矩形也不是圆形,而“正方形具有面积”它是现有尺寸之一的长度乘以自身的长度,无论您拥有什么正方形,如果它是2D空间中的正方形,则它的面积绝对不能是任何东西,但其长度是平方的,证明它几乎是微不足道的。我们不需要断言以确保我们的世界是这样,我们只是利用现实的含义来防止我们的程序偏离轨道。

我几乎可以肯定是错的,但是我认为这可以帮助某个人,因此希望它可以对某个人有所帮助。


5

在Scala的上下文中,您将发现以下内容是最简单的定义。基本上,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
  }
}

例如

// 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)

注:严格地说一个定义函数式编程单子是不一样的一个定义范畴论单子,这在圈中定义mapflatten。尽管在某些映射下它们是等效的。此演示文稿非常好:http : //www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category


5

这个答案从一个有启发性的示例开始,贯穿该示例,得出一个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负责将自己的日志消息附加到先前的日志记录信息的事实。(仅出于争辩的考虑,而不是附加字符串,f并且g必须在该对的第二项上执行复杂的逻辑。要在两个或更多不同的函数中重复该复杂的逻辑会很痛苦。)

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

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>成功能f通过 x进入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 _在您用类型填充空白后引用类型。An M int是一对int和一个字符串。An M string是一对字符串和一个字符串。等等。

恭喜,您创建了一个monad!

正式地,您的monad是元组<M, feed, wrap>

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

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

tuv是三种可能相同或不同的类型。一个monad满足您为特定monad证明的三个属性:

  • 馈送包裹t成一个函数是相同的传递展开的t入功能。

    正式地: feed(f, wrap(x)) = f(x)

  • 喂食的M twrap什么都不做的M t

    正式地: feed(wrap, m) = m

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

    • tg
    • M u(得到ng
    • 饲料nf

    是相同的

    • 馈送 mg
    • 获得ng
    • 饲养nf

    正式地:feed(h, m) = feed(f, feed(g, m))在哪里h(x) := feed(f, g(x))

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


5

我将尝试Monad在Haskell的背景下进行解释。

在函数编程中,函数组成很重要。它允许我们的程序包含小的,易于阅读的功能。

假设我们有两个功能:g :: Int -> Stringf :: String -> Bool

我们可以做(f . g) x,与一样f (g x),其中x一个Int值。

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

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

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

我们不能做(f1 . g1) xf1 (g1 x),其中x是一个Int值。返回的结果类型g1不是什么f1预期的。

我们可以撰写fg.运营商,但现在我们不能组成f1g1使用.。问题在于,我们无法将上下文中的值直接传递给期望不在上下文中的值的函数。

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

我们也有 >>=尽管语法略有不同,但运算符可以为我们做完全相同的事情。

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

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

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

如果这里要记住一件事,那就是Monad允许函数组合涉及上下文中的值



IOW,Monad是广义函数调用协议。
Will Ness

在我看来,您的回答是最有帮助的。尽管我不得不说,我认为重点应该放在这样一个事实上,即您所引用的函数不仅在上下文中包含值,而且它们会在上下文中积极地放置值。因此,例如,一个函数f :: ma-> mb很容易与另一个函数g :: mb-> m c组成。但是monads(特别是绑定)使我们能够永久地构成将其输入置于相同上下文中的函数,而无需我们首先从该上下文中取出值(这将有效地从值中删除信息)
James

@James我认为这应该是函子的重点?
乔纳斯(Jonas)

@Jonas我想我没有正确解释。当我说这些函数将值放在上下文中时,我的意思是它们具有类型(a-> mb)。这些非常有用,因为将值放入上下文中会为其添加新信息,但是通常很难将(a-> mb)和(b-> mc)链接在一起,因为我们不能仅仅将值取出来的上下文。因此,我们将不得不使用一些复杂的过程来根据特定的上下文以明智的方式将这些功能链接在一起,而monad仅允许我们以一致的方式执行此操作,而不管上下文如何。
詹姆斯

5

tl; dr

{-# 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是左右身份。

克莱斯里三人组

在编程中,monad是函子类型构造函数,具有monad类型类的实例。定义和实现有几种等效的变体,每种变体对monad抽象的理解都略有不同。

一个仿函数是一种构造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

Monads是按照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成一元上下文mExtensionKleisli应用程序 =<<将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类型类的实例必须遵守monad法则,就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中,标识monad被定义

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计算进行编码,这可能会“失败”。选项monad已定义

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 -?

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

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

清单

列表单子,在列表类型之上

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

infixr 5 :

并且其附加的monoid操作“附加”

(++) :: [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]由应用程序产生f x一个Kleisli的箭头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   =   []

在定义monad类型类而不是扩展时=<<,Haskell标准使用其翻转字符bind运算符>>=

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

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

使用列表monad,命令伪代码

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

等效的monad理解

[ 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
      )
   )

注解和monad理解是嵌套绑定表达式的语法糖。bind操作符用于单子结果的本地名称绑定。

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

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

臭名昭著的是,使用monad对状态计算进行编码。

状态处理器是一个函数

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

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

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

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

状态处理程序monad是仁慈的* -> *函子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) 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声明状态类型对monad 的功能依赖 ; 一个,例如,将确定的状态类型是唯一。stmState tt

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

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

void在C中类似使用的单位类型。

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 通常与记录字段访问器一起使用。

状态monad等效于变量线程

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                 }
}                    }

像在面向表达式的编程(例如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 = 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世界状态处理器monad是纯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原语的代码的杂物由类型系统永久地协议化。因为纯净真棒,所以发生在里面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)

其中XY在对象CHomC(X, Y)同态类的所有态射的X -> YC。仿函数必须保留态射特性和组合物,所述的“结构” CD

                    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, _*>

终结者的

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

也可以mu在编程中称为Monad,而不是根据Kleislian扩展定义而是自然转换join。monad被定义muCendofunctor 的category 的三元组

     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

然后定义monad类型类

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

mu选项monad 的规范实现:

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清单monad的。

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

但是,为什么这么抽象的理论对编程有什么用呢?

答案很简单:作为计算机科学家,我们重视抽象!在设计软件组件的接口时,我们希望它尽可能少地揭示实现。我们希望能够用许多替代方案,同一“概念”的许多其他“实例”替代实现。当我们为许多程序库设计通用接口时,选择的接口具有多种实现就显得尤为重要。我们非常重视的是monad概念的普遍性,这是因为类别理论是如此抽象,以至于其概念对于编程非常有用。

因此,我们在下面介绍的monad的泛化也与范畴论有着密切的联系,这几乎是令人惊讶的。但是我们强调我们的目的是非常实际的:不是“实现类别理论”,而是找到一种更通用的构造组合器库的方法。数学家们已经为我们完成了许多工作,这仅仅是我们的幸运!

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


4

世界需要的是另一篇有关monad的博客文章,但我认为这对于确定野外现有的monad很有用。

谢尔宾斯基三角形

上面的分形叫做Sierpinski三角形,这是我能记得绘制的唯一分形。分形是自相似的结构,如上述三角形,其中各部分与整体相似(在这种情况下,比例只是父三角形的一半)。

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


3
您是说“世界不需要什么……”吗?很好的比喻!
groverboy

@ icc97您说得对-含义很清楚。讽刺意料之外,向作者道歉。
groverboy

世界需要的是另一个评论线程,它证实了一种讽刺意味,但是如果仔细阅读,我已经写过,但是应该清楚。
尤金(Eugene Yokota)


4

让下面的“ {| 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试图求一个单子,但被斥责了。

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

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

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

鲜为人知的是f,单子和>>=正在勾结。

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

但是他们实际上在谈论什么呢?好吧,这取决于单子。仅以抽象方式交谈具有有限的用途;您必须对特定的单子有一定的经验才能充实理解。

例如,数据类型Maybe

 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}

因此,如果Mayad monad实际上包含a它要通告的内容,则可以让计算继续进行,但如果不包含它,则中止计算。结果仍然是单子数据,尽管不是的输出f。因此,可能使用Maybe monad表示失败的上下文。

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

(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  

在这种情况下,该函数知道如何根据其输入创建列表,但不知道如何处理额外的输入和额外的列表。bind >>=f通过组合多个输出来提供帮助。我包含此示例,以说明尽管>>=它负责提取a,但它也可以访问的最终绑定输出f。实际上,a除非知道最终输出具有相同类型的上下文,否则它将永远不会提取任何内容。

还有其他monad用于表示不同的上下文。这里是一些其他特征。该IO单子实际上并没有一个a,但它知道一个家伙,会得到a你。该State stmonad有一个秘密藏身之处,即使它刚要了st,它也会传递到f桌子底下。该单子类似,虽然它只是让看。faReader rState stfr

所有这些的意义在于,任何本身声明为Monad的数据类型都在声明从monad提取值的某种上下文。从这一切中获得最大收益?好吧,它很容易在某种上下文中进行计算。但是,当将多个上下文加载计算串在一起时,可能会变得混乱。monad操作负责解决上下文的交互,因此程序员不必这样做。

请注意,使用>>=减轻了的部分自治,从而减轻了混乱f。也就是说,例如在上述情况Nothing下,f不再需要决定在的情况下该怎么做Nothing;它编码为>>=。这是权衡。如果有必要f决定在情况下该怎么做Nothing,则f应该是from Maybe a到函数Maybe b。在这种情况下,Maybe成为单子无关紧要。

但是请注意,有时数据类型不会导出它的构造函数(在IO上看),并且如果我们要使用广告值,则别无选择,只能使用它的monadic接口。


3

monad是用于封装状态变化的对象的东西。在其他不允许您具有可修改状态的语言(例如,Haskell)中最经常遇到这种情况。

例如文件I / O。

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


3
据我了解,单子不仅限于此。用“纯”功能语言封装可变状态只是monad的一种应用。
thSoft 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.