haskell -什么是单子?

Translate

最近简要查看过Haskell后,简短,简洁,实用关于什么是monad的解释?

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

This question and all comments follow the "Attribution Required."

所有的回答

Translate

第一:学期单子如果您不是数学家,那就有点虚无。一个替代术语是计算生成器这更能说明它们的实际用途。

您要求提供实际示例:

示例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计算生成器。共同的主题是单子连锁经营以一些特定的,有用的方式。在列表理解中,操作被链接在一起,这样,如果操作返回列表,则在以下位置执行以下操作每一项在列表中。另一方面,IO monad顺序执行操作,但传递一个“隐藏变量”,代表“世界的状态”,这使我们能够以纯功能的方式编写I / O代码。

事实证明连锁经营非常有用,可用于Haskell中的许多不同事物。

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

列表理解语法和do-notation都是用于使用>>=操作员。一个monad基本上只是一种支持>>=操作员。

示例3:解析器

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

parseExpr = parseString <|> parseNumber

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

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

行动char, digit等非常简单。他们要么匹配,要么不匹配。魔力是管理控制流程的monad:操作顺序执行,直到匹配失败,在这种情况下monad会回溯到最新的<|>并尝试下一个选项。同样,这是一种将操作与一些其他有用的语义链接在一起的方式。

示例4:异步编程

上面的示例在Haskell中,但事实证明F#也支持单子。这个例子是从唐·塞姆:

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 {}语法表示该块中的控制流由async单子。)

他们如何工作

那么,monad如何做所有这些花哨的控制流程呢?在do-block(或计算表达式正如在F#中所称),是每个操作(基本上每行)都包装在单独的匿名函数中。然后使用bind运算符(拼写>>=在Haskell)。自从bind操作结合了函数,可以按照自己的意愿执行它们:依次,多次,相反地,丢弃某些函数,并在感觉上在单独的线程上执行某些函数,依此类推。

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

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

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

注意>>=是针对不同类型的重载,因此每个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

来源
Translate

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

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

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

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

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

好了,绑定功能能够只需从一个步骤中获取结果,然后将其提供给下一步即可。但是,如果那是“全部”,那单子做的……实际上不是很有用。理解这一点很重要:每个有用monad还有其他事情此外成为一个单子。每一个有用monad具有“特殊功能”,这使其与众不同。

(一个单子没有特殊的被称为“身份单子”。就像身份功能一样,这听起来似乎毫无意义,但事实并非如此……但这是另一个故事™。)

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

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

  • 扩展这个想法,您可以实现“异常”。 (错误单子要么例外莫纳德。)因为您是在定义它们而不是语言功能,所以可以定义它们的工作方式。 (例如,您可能想忽略前两个例外,而仅在第三引发异常。)

  • 您可以使每一步都返回多个结果,并使bind函数循环遍历它们,为您提供下一步的服务。这样,在处理多个结果时,您不必一直在各处编写循环。绑定功能“自动”为您完成所有操作。 (列出单子.)

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

  • 您可以这样做,以便可以替换“额外数据”。这使您能够模拟破坏性更新,而不实际进行破坏性更新。 (州立莫纳德及其表亲作家莫纳德.)

  • 因为你只是模拟破坏性的更新,您可以琐碎地做一些事真实破坏性的更新。例如,您可以撤消上一次更新, 要么恢复到旧版本.

  • 您可以在其中进行计算的单子已暂停,因此您可以暂停程序,进入并修改内部状态数据,然后继续执行。

  • 您可以将“ continuations”实现为monad。这使您能够让人心碎!

使用monad可以实现所有这些以及更多功能。当然,所有这些都是完全可能的没有单子也。只是彻底更轻松使用单子。

来源
Translate

实际上,与对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内部.

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

来源
Translate

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

sigfpe说:

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

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

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

来源
Translate

monad是具有两种操作的数据类型:>>=(又名bind)和return(又名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,异常和不确定性。列表和选项类型可能是最容易理解的单子了:

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代码,以了解monad的含义以及它们为什么有用的原因。

来源
Translate

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

A 高阶函数只是一个以函数为参数的函数。

A 函子是任何类型的构造T对于存在高阶函数的函数map,转换类型的函数a -> b(考虑到任何两种类型ab)成一个函数T a -> T b。这个map函数还必须遵守身份和组成定律,以便以下表达式对所有pq(Haskell表示法):

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

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

A 单子本质上只是一个函子T还有两种额外的方法join,类型T (T a) -> T aunit(有时称为return, fork, 要么pure)类型a -> T a。对于Haskell中的列表:

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

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

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

一个单子必须满足某些法律,即join必须是关联的。这意味着如果您有价值x类型的[[[a]]]然后join (join x)应该相等join (map join x)。和pure必须是join这样join (pure x) == x.

来源
Translate

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

阿尔纳尔写道:

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

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

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

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

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

    例如,一个Maybe Int可以是Just Int要么Nothing。现在,如果您添加一个Maybe Int到一个Maybe Int,操作员将检查两者是否都Just Int在里面,如果是这样,将打开Ints,向他们传递加法运算符,然后重新包装结果Int变成新的Just Int(这是有效的Maybe Int),然后返回一个Maybe Int。但是如果其中一个是Nothing在里面,这个运算符将立即返回Nothing,这又是有效的Maybe Int。这样,您可以假装Maybe Ints只是普通数字,并对它们执行常规数学运算。如果你要得到一个Nothing,您的方程式仍将产生正确的结果–无需乱扔垃圾检查Nothing到处.

但是这个例子就是发生了什么Maybe。如果额外的信息是IO,则该特殊运算符为IO而是调用s,它在执行加法之前可能会做一些完全不同的事情。 (确定,添加两个IO Int在一起可能很荒谬–我不确定。)(此外,如果您注意了Maybe例如,您已经注意到“用多余的东西包装价值”并不总是正确的。但是,如果无法理解,就很难做到准确,正确和精确。)

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

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

但是,请考虑在语言中为模式概念提供语法支持的含义:而不是必须阅读语法四人帮预定并记住特定图案的构造,编写以不可知论的通用方式实现此模式的代码一次,您就完成了!然后,您只需使用代码装饰代码中的操作即可重复使用此模式,例如Visitor或Strategy或Façade或其他内容,而无需一遍又一遍地重新实现!

所以这就是为什么理解单子找到他们有用:不是知识分子势利的人以理解为荣,这不是象牙塔的概念(当然,当然也是如此),但实际上使代码更简单。

来源
Translate

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

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

  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”:(bind)会自动将其左侧的返回值送入其右侧的函数参数,以及lift运算符,可将正常功能转换为与该特定种类的绑定运算符一起使用的功能。

monad如何实现?

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

来源
Translate

(另请参见什么是单子?)

sigfpe(Dan Piponi)的灵感来自Monads您可能已经发明了Monad! (也许您已经拥有了)。有很多其他monad教程,其中许多错误地尝试使用各种类比以“简单的术语”来解释单子。monad教程谬论;避免他们。

正如Maciver博士所说告诉我们为什么你的语言很烂:

因此,我讨厌Haskell:

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

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

[如果您是数学方面的专家,则可能要忽略许多教程并了解其定义,或者遵循范畴理论讲座:)定义的主要部分是Monad M涉及一个“类型构造函数”,该类型构造函数为每个现有类型“ T”定义一个新类型“ MT”,以及在“常规”类型和“ M“类型。]

同样,令人惊讶的是,有关monads的最佳介绍之一实际上是介绍monads的早期学术论文之一,Philip Wadler的用于函数式编程的Monad。它实际上具有实用性不平凡的具有启发性的示例,与现有的许多人工教程不同。

来源
Translate

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

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

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

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

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

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

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

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

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

可以通过重组输入来改进此设计,以便更轻松地查询相关状态。所以,不只是简单y价值可以变成Mb例如(is_OK, b)如果y包括有效性的概念。

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

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

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

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

为了实现这种外在化,我们可以使用一个函数,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. a bind实现的功能是在将组合函数应用于它在外壳中找到的内容值时利用此外壳信息,以及
  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 a
Right Identity : Ma >>= return    === Ma
Associative    : Ma >>= (f >>= g) === Ma >>= ((fun x -> f x) >>= g)

Associativity意思是bind保留评估顺序,无论何时bind被申请;被应用。也就是说,在定义Associativity以上,对力进行早期评估的括号bindingfg只会产生预期的功能Ma为了完成bind。因此,对Ma必须先确定其值才能应用于f然后将结果应用于g.

来源
Tim Lee
Translate

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

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

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

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

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

例如,“ if”单子:

if( clause ) then block

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

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

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

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

来源
Translate

我最喜欢的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”)可以让您使用花括号,分号和/或缩进和换行符。

来源
Camille Lee
Translate

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

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

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

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

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

来源
Translate

我仍然对monad还是陌生的,但是我想分享一个链接,我觉得阅读起来真的很不错(有图片!!):http://www.matusiak.eu/numerodix/blog/2012/3/11/monads-for-the-layman/(无从属关系)

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

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

来源
Elliot Lee
Translate

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

jQuery文档本身不是指“ monad”一词,而是谈论可能更熟悉的“构建器模式”。这并不会改变您可能没有意识到的那一面适当的monad的事实。

来源
Esther Lee
Translate

单子不是隐喻,但正如Daniel Spiewak解释的那样,它是从通用模式中产生的实用实用抽象。

来源
Duncan Lee
Translate

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

来源
Boris Lee
Translate

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

来源
Max Lee
Translate

如果我正确理解,则IEnumerable是从monad派生的。我想知道对于C#世界中的那些人来说这是否可能是一个有趣的方法?

值得一提的是,这里有一些教程的链接对我有帮助(并且不,我仍然不知道单子是什么)。

来源
Clarence Lee
Translate

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

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

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

来源
Deborah Lee
Translate

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。一个单子似乎是一个“可理解的单位,可以通过完全理解来预测” –如果您理解“也许”单子,那么除了“也许”这看起来是微不足道的,它不可能做任何事情,但是在大多数非单子中代码,一个简单的功能“ helloworld”可以发射导弹,什么也不做,甚至破坏宇宙甚至扭曲时间-我们不知道,也没有任何保证。 monad保证它就是它。这非常强大。

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

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

来源
Translate

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

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

来源
Translate

这个答案从一个有启发性的示例开始,贯穿该示例,得出一个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“。 至饲料一双<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,那么您的货币对就是“ pair of of”类型的值t和字符串”。调用该类型M t.

M是类型构造函数:M单独不指类型,但M _在您将类型填入空白后,便指代一种类型。一个M int是一对整数和一个字符串。一个M string是一对字符串和一个字符串。等等。

恭喜,您创建了monad!

正式来说,你的单子是元组<M, feed, wrap>.

单子是一个元组<M, feed, wrap>哪里:

  • M是类型构造函数。
  • feed需要一个t并返回一个M u) 和M t并返回一个M u.
  • wrap需要一个v并返回一个M v.

t, uv是可能相同也可能不相同的任何三种类型。一个monad满足您为特定monad证明的三个属性:

  • 喂食一个包裹t变成一个函数是一样的通过解开的t进入功能。

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

  • 喂养M t进入wrap什么都不做M t.

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

  • 喂养M t(称它为m)转化为

    • 通过t进入g
    • 得到一个M u(称它为n)来自g
    • 饲料n进入f

    是相同的

    • 喂食m进入g
    • 得到ng
    • 喂食n进入f

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

通常,feed叫做bind(又名>>=在Haskell)和wrap叫做return.

来源
Translate

我会尽力解释Monad在Haskell的背景下。

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

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

我们能做的(f . g) x,与f (g x),在哪里x是一个Int值。

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

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

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

我们做不到(f1 . g1) x要么f1 (g1 x),在哪里x是一个Int值。返回的结果类型g1不是什么f1期望。

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

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

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

我们写:g1 x >>= f1. g1 x是一个Maybe Int值。的>>=操作员可以帮助您Int从“可能不存在”的上下文中评估价值,并将其应用于f1。的结果f1,这是一个Maybe Bool,将是整个>>=操作。

最后,为什么Monad有用?因为Monad是定义>>=运算符,与Eq定义==/=操作员。

总而言之,Monad类型类定义了>>=运算符,该运算符使我们可以将上下文中的值(我们称为单子值)传递给在上下文中不期望值的函数。上下文将得到照顾。

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

来源
Translate

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 x (infixl 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.

一元单子函数或克莱斯利箭有类型

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

Kleisi箭头是带有一个参数的函数a并返回一元计算m b.

Monad的规范定义是克莱斯利三人组 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 =<<

克莱斯里身份 return是提升价值的Kleisli箭头t进入单子语境m. 延期要么Kleisli应用 =<<应用Kleisli箭头a -> m b计算结果m a.

克莱斯里组成 <=<根据扩展定义为

(<=<) :: 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 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克莱斯里箭头的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标准使用其翻转功能捆绑算子>>=.

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也都是应用程序。

使用list monad,命令式伪代码

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

大致翻译成做块,

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

注解和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

应用于结果()。如果为假,则警卫人员生成列表单子的fail[]),则应用Kleisli箭头不会产生任何结果>>到,所以这个p被跳过。

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

A 状态处理器是一个功能

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声明一个功能依赖状态类型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)

其单位类型类似于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)

or

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)

改变世界的功能。

结语

类别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)

哪里X, Y是在C. HomC(X, Y)是个同态类所有的态射X -> YC。函子必须保留态素的身份和组成,即C,在D.

                    Tmor    Tobj

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

克莱斯里类别类别C由Kleisli三元组给出

<T, eta, _*>

终结者的

T : C -> C

(f),一个身份形态eta (return)和扩展运算符* (=<<).

每个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

Monads也可以不根据Kleislian扩展定义,而是自然转换mu,在编程中称为join。 monad的定义是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

然后定义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单子列表中。

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的泛化也与范畴理论有着密切的联系,这几乎让人感到惊讶。但是我们强调我们的目的是非常实际的:不是“实现类别理论”,而是找到一种构造组合器库的更通用的方法。数学家已经为我们完成了许多工作,这仅仅是我们的幸运!

将Monad推广为箭通过约翰休斯

来源
Yale Lee
Translate

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

Sierpinski triangle

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

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

来源
Translate
Translate

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

例如文件I / O。

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

来源
Translate

让下面的“{| 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 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  

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

还有其他monad用于表示不同的上下文。这里是一些其他特征。的IOmonad实际上没有a,但它认识一个人,并且会得到那个a为了你。的State stmonad有一个秘密藏身之处st它将传递给f在桌子底下,即使f刚来要a。的Reader rmonad类似于State st,尽管它只允许f看着r.

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

注意,使用>>=通过消除某些自主权来缓解混乱f。也就是说,在上述情况下Nothing例如,f不再需要决定如何处理Nothing;它被编码为>>=。这是权衡。如果有必要f决定在以下情况下该怎么做Nothing, 然后f应该是来自Maybe aMaybe b。在这种情况下,Maybe作为一个单子是无关紧要的。

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

来源