最近对 Haskell 进行了简要介绍,对于 monad 本质上是什么,有什么简短、简洁、实用的解释?
我发现我遇到的大多数解释都相当难以理解并且缺乏实际细节。
最近对 Haskell 进行了简要介绍,对于 monad 本质上是什么,有什么简短、简洁、实用的解释?
我发现我遇到的大多数解释都相当难以理解并且缺乏实际细节。
首先:如果你不是数学家, monad这个词有点空洞。另一个术语是计算构建器,它更能描述它们的实际用途。
它们是链接操作的模式。它看起来有点像面向对象语言中的方法链接,但机制略有不同。
该模式主要用于函数式语言(尤其是普遍使用 monad 的 Haskell),但可以用于任何支持高阶函数的语言(即可以将其他函数作为参数的函数)。
JavaScript 中的数组支持该模式,所以让我们将其用作第一个示例。
该模式的要点是我们有一个类型(Array
在这种情况下),它有一个将函数作为参数的方法。提供的操作必须返回一个相同类型的实例(即返回一个Array
)。
首先是一个不使用 monad 模式的方法链接示例:
[1,2,3].map(x => x + 1)
结果是[2,3,4]
。代码不符合 monad 模式,因为我们作为参数提供的函数返回一个数字,而不是一个数组。monad 形式的相同逻辑是:
[1,2,3].flatMap(x => [x + 1])
这里我们提供了一个返回 an 的操作Array
,所以现在它符合模式。该flatMap
方法为数组中的每个元素执行提供的函数。它期望一个数组作为每次调用的结果(而不是单个值),但是将结果集合并到一个数组中。所以最终结果是一样的,数组[2,3,4]
。
(提供给类似map
或方法的函数参数flatMap
在 JavaScript 中通常称为“回调”。我将其称为“操作”,因为它更通用。)
如果我们链接多个操作(以传统方式):
[1,2,3].map(a => a + 1).filter(b => b != 3)
数组中的结果[2,4]
monad 形式的相同链接:
[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])
产生相同的结果,数组[2,4]
。
您会立即注意到 monad 形式比非 monad 更难看!这只是表明单子不一定是“好”的。它们是一种有时有益有时无益的模式。
请注意,monad 模式可以以不同的方式组合:
[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))
这里的绑定是嵌套的而不是链式的,但结果是一样的。这是我们稍后会看到的 monad 的一个重要属性。这意味着两个操作组合在一起可以被视为一个单一的操作。
允许该操作返回具有不同元素类型的数组,例如将数字数组转换为字符串数组或其他内容;只要它仍然是一个数组。
这可以使用 Typescript 表示法更正式地描述。数组具有 type Array<T>
,其中T
是数组中元素的类型。该方法flatMap()
接受该类型的函数参数T => Array<U>
并返回一个Array<U>
.
概括地说,monad 是Foo<Bar>
具有“绑定”方法的任何类型,该方法接受类型的函数参数Bar => Foo<Baz>
并返回Foo<Baz>
.
这回答了monad 是什么。这个答案的其余部分将尝试通过示例来解释为什么 monad 在像 Haskell 这样对它们有很好支持的语言中可以成为一种有用的模式。
Haskell 和 Do-notation
要将 map/filter 示例直接转换为 Haskell,我们flatMap
用>>=
运算符替换:
[1,2,3] >>= \a -> [a+1] >>= \b -> if b == 3 then [] else [b]
运算符是 Haskell 中的>>=
绑定函数。当操作数是一个列表时,它的作用与 JavaScript 中的相同flatMap
,但对于其他类型,它具有不同的含义。
但是 Haskell 也有一个用于 monad 表达式的专用语法do
-block,它完全隐藏了绑定运算符:
do a <- [1,2,3]
b <- [a+1]
if b == 3 then [] else [b]
这隐藏了“管道”,让您专注于每一步应用的实际操作。
在一个do
块中,每一行都是一个操作。约束仍然认为块中的所有操作都必须返回相同的类型。由于第一个表达式是一个列表,其他操作也必须返回一个列表。
后退箭头<-
看起来像是一个赋值,但请注意这是绑定中传递的参数。因此,当右侧的表达式是整数列表时,左侧的变量将是单个整数——但将对列表中的每个整数执行。
示例:安全导航(Maybe 类型)
列表说得够多了,让我们看看 monad 模式如何对其他类型有用。
某些函数可能并不总是返回有效值。在 Haskell 中,这由Maybe
-type 表示,它是一个选项,要么是Just value
要么Nothing
。
总是返回有效值的链接操作当然很简单:
streetName = getStreetName (getAddress (getUser 17))
但是如果任何函数都可以返回Nothing
呢?我们需要单独检查每个结果,如果不是,则仅将值传递给下一个函数Nothing
:
case getUser 17 of
Nothing -> Nothing
Just user ->
case getAddress user of
Nothing -> Nothing
Just address ->
getStreetName address
相当多的重复检查!想象一下,如果链条更长。Haskell 用 monad 模式解决了这个问题Maybe
:
do
user <- getUser 17
addr <- getAddress user
getStreetName addr
这个do
-block 调用Maybe
类型的绑定函数(因为第一个表达式的结果是 a Maybe
)。如果值为 ,则绑定函数仅执行以下操作Just value
,否则它只是传递Nothing
。
这里使用 monad-pattern 来避免重复代码。这类似于一些其他语言如何使用宏来简化语法,尽管宏以非常不同的方式实现相同的目标。
请注意,是monad 模式和 Haskell 中对 monad 友好的语法的组合产生了更简洁的代码。在像 JavaScript 这样的语言中,对 monad 没有任何特殊的语法支持,我怀疑 monad 模式在这种情况下是否能够简化代码。
可变状态
Haskell 不支持可变状态。所有变量都是常量,所有值都是不可变的。但是该State
类型可用于模拟具有可变状态的编程:
add2 :: State Integer Integer
add2 = do
-- add 1 to state
x <- get
put (x + 1)
-- increment in another way
modify (+1)
-- return state
get
evalState add2 7
=> 9
该add2
函数构建一个 monad 链,然后以 7 作为初始状态进行评估。
显然,这仅在 Haskell 中才有意义。其他语言支持开箱即用的可变状态。Haskell 通常在语言特性上“选择加入”——您在需要时启用可变状态,并且类型系统确保效果是明确的。IO 是另一个例子。
IO
该IO
类型用于链接和执行“不纯”函数。
与任何其他实用语言一样,Haskell 有一堆与外界交互的内置函数:putStrLine
等等readLine
。这些函数被称为“不纯”,因为它们要么会导致副作用,要么会产生不确定的结果。即使是像获取时间这样简单的事情也被认为是不纯的,因为结果是不确定的——用相同的参数调用它两次可能会返回不同的值。
纯函数是确定性的——它的结果完全取决于传递的参数,除了返回一个值之外,它对环境没有副作用。
Haskell 大力鼓励使用纯函数——这是该语言的一个主要卖点。不幸的是,对于纯粹主义者来说,你需要一些不纯的函数来做任何有用的事情。Haskell 的折衷方案是将纯函数和不纯函数清晰地分开,并保证纯函数无法直接或间接执行不纯函数。
这是通过为所有不纯函数指定IO
类型来保证的。Haskell 程序的入口点是main
具有IO
类型的函数,因此我们可以在顶层执行不纯函数。
但是语言如何防止纯函数执行不纯函数呢?这是由于 Haskell 的惰性。一个函数只有在它的输出被其他函数消耗时才会被执行。但是没有办法使用一个IO
值,除非将它分配给main
. 所以如果一个函数想要执行一个不纯的函数,它必须被连接main
并具有IO
类型。
对 IO 操作使用 monad 链接还可以确保它们以线性和可预测的顺序执行,就像命令式语言中的语句一样。
这将我们带到大多数人会用 Haskell 编写的第一个程序:
main :: IO ()
main = do
putStrLn ”Hello World”
当do
只有一个操作并且没有要绑定的内容时,关键字是多余的,但为了保持一致性,我还是保留了它。
类型的()
意思是“空的”。这种特殊的返回类型仅对因其副作用而调用的 IO 函数有用。
一个更长的例子:
main = do
putStrLn "What is your name?"
name <- getLine
putStrLn "hello" ++ name
这构建了一个IO
操作链,并且由于它们被分配给main
函数,它们被执行。
比较IO
显示Maybe
了单子模式的多功能性。对于Maybe
,该模式用于通过将条件逻辑移动到绑定函数来避免重复代码。对于IO
,该模式用于确保该IO
类型的所有操作都是有序的,并且IO
操作不能“泄漏”给纯函数。
加起来
在我的主观意见中,monad 模式只有在对模式有一些内置支持的语言中才真正值得。否则只会导致代码过于复杂。但是 Haskell(和其他一些语言)有一些内置的支持,隐藏了繁琐的部分,然后该模式可以用于各种有用的事情。喜欢:
Maybe
)IO
)Parser
)解释“什么是单子”有点像说“什么是数字?” 我们一直使用数字。但是想象一下,你遇到了一个对数字一无所知的人。你怎么解释数字是什么?你甚至会如何开始描述为什么这可能有用?
什么是单子?简短的回答:这是一种将操作链接在一起的特定方式。
本质上,您正在编写执行步骤并将它们与“绑定函数”链接在一起。(在 Haskell 中,它被命名为>>=
。)您可以自己编写对绑定运算符的调用,也可以使用语法糖,让编译器为您插入这些函数调用。但无论哪种方式,每个步骤都通过调用此绑定函数来分隔。
所以 bind 函数就像一个分号;它将过程中的步骤分开。绑定函数的工作是获取上一步的输出,并将其馈送到下一步。
这听起来不太难,对吧?但是有不止一种单子。为什么?如何?
好吧,绑定函数可以只从一个步骤中获取结果,并将其提供给下一步。但是,如果这就是 monad 所做的“全部”……那实际上并不是很有用。理解这一点很重要:每个有用的 monad除了只是一个 monad之外还做其他事情。每个有用的 monad 都有一种“特殊的力量”,这使它独一无二。
(没有什么特别作用的 monad 被称为“identity monad”。很像身份函数,这听起来完全没有意义,但事实证明不是……但那是另一回事了™。)
基本上,每个 monad 都有自己的 bind 函数实现。而且您可以编写一个绑定函数,以便它在执行步骤之间做一些复杂的事情。例如:
如果每个步骤都返回一个成功/失败指示符,那么您可以让 bind 仅在前一个步骤成功时才执行下一步。这样,失败的步骤会“自动”中止整个序列,而无需您进行任何条件测试。(失败单子。)
扩展这个想法,你可以实现“异常”。(错误单子或异常单子。)因为您自己定义它们而不是语言功能,所以您可以定义它们的工作方式。(例如,也许您想忽略前两个异常,仅在抛出第三个异常时中止。)
您可以让每个步骤返回多个结果,并让 bind 函数循环它们,为您提供每个步骤。这样,在处理多个结果时,您不必到处写循环。绑定功能“自动”为您完成所有这些工作。(列表单子。)
除了将“结果”从一个步骤传递到另一个步骤之外,您还可以让绑定函数传递额外的数据。这些数据现在不会显示在您的源代码中,但您仍然可以从任何地方访问它,而无需手动将其传递给每个函数。(读者单子。)
您可以这样做,以便可以替换“额外数据”。这允许您模拟破坏性更新,而无需实际进行破坏性更新。(状态单子及其表亲作家单子。)
因为您只是在模拟破坏性更新,所以您可以轻松地完成真正的破坏性更新不可能完成的事情。例如,您可以撤消上次更新,或恢复到旧版本。
你可以创建一个可以暂停计算的 monad ,这样你就可以暂停你的程序,进入并修改内部状态数据,然后恢复它。
您可以将“延续”实现为单子。这可以让你打破人们的想法!
使用 monad 可以实现所有这些以及更多。当然,如果没有monad,这一切也是完全可能的。使用单子要容易得多。
实际上,与对 Monad 的普遍理解相反,它们与状态无关。Monads 只是一种包装事物的方法,并提供了在不打开包装的情况下对包装的东西进行操作的方法。
例如,您可以在 Haskell 中创建一个类型来包装另一个类型:
data Wrapped a = Wrap a
包装我们定义的东西
return :: a -> Wrapped a
return x = Wrap x
要在不展开的情况下执行操作,假设你有一个 function f :: a -> b
,那么你可以这样做来提升该函数以对包装的值起作用:
fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)
这就是要了解的所有内容。然而,事实证明,有一个更通用的函数来做这个提升,它是bind
:
bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x
bind
可以做多一点fmap
,但反之则不行。实际上,fmap
只能用bind
和来定义return
。所以,当定义一个 monad.. 你给它的类型(这里是Wrapped a
),然后说它return
和bind
操作是如何工作的。
很酷的是,这竟然是一个如此普遍的模式,它无处不在,以纯粹的方式封装状态只是其中之一。
有关如何使用 monads 来引入函数依赖并因此控制评估顺序的好文章,就像在 Haskell 的 IO monad 中使用的那样,请查看IO Inside。
至于理解 monad,不用太担心。阅读他们感兴趣的内容,如果您不立即理解,请不要担心。然后只是潜入像 Haskell 这样的语言是要走的路。单子是其中一种通过练习,理解会慢慢渗入你的大脑,有一天你突然意识到你理解了它们。
sigfpe 说:
但是所有这些都将 monad 作为需要解释的深奥事物引入。但我要争辩的是,它们根本不是深奥的。事实上,面对函数式编程中的各种问题,你会被无情地引导到某些解决方案,所有这些都是单子的例子。事实上,如果你还没有,我希望让你现在就发明它们。只需一小步就能注意到所有这些解决方案实际上都是变相的相同解决方案。读完这篇文章后,你可能会更好地理解其他关于 monad 的文档,因为你会认识到你所看到的一切都是你已经发明的东西。
monad 试图解决的许多问题都与副作用问题有关。所以我们将从他们开始。(请注意,monad 让您做的不仅仅是处理副作用,特别是许多类型的容器对象可以被视为 monad。一些对 monad 的介绍发现很难协调 monad 的这两种不同用途并只专注于一种或另一个。)
在诸如 C++ 之类的命令式编程语言中,函数的行为与数学函数完全不同。例如,假设我们有一个 C++ 函数,它接受一个浮点参数并返回一个浮点结果。从表面上看,它可能看起来有点像将实数映射到实数的数学函数,但 C++ 函数可以做的不仅仅是返回一个取决于其参数的数字。它可以读取和写入全局变量的值,也可以将输出写入屏幕并接收用户的输入。然而,在纯函数式语言中,函数只能读取其参数中提供给它的内容,并且它对世界产生影响的唯一方法是通过它返回的值。
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
并且Nothing
是Maybe
构造函数。这两个 monad 都封装了针对其各自数据类型的通用且有用的计算模式(请注意,它们都与副作用或 I/O 无关)。
您确实必须尝试编写一些重要的 Haskell 代码来了解 monad 的含义以及它们为何有用。
您应该首先了解函子是什么。在此之前,了解高阶函数。
高阶函数只是一个将函数作为参数的函数。
函子T
是任何存在高阶函数的类型构造,称为它,它将map
类型的函数a -> b
(给定任何两种类型a
和b
)转换为函数T a -> T b
。此map
函数还必须遵守恒等律和组合律,以便以下表达式对所有p
和q
(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)
为每个元素调用函数,并返回结果列表。
monad本质上只是一个具有两个额外方法的函子,T
类型join
为T (T a) -> T a
, 和类型unit
(有时称为return
,fork
或pure
)a -> T a
。对于 Haskell 中的列表:
join :: [[a]] -> [a]
pure :: a -> [a]
为什么有用?因为例如,您可以map
使用返回列表的函数覆盖列表。Join
获取列表的结果列表并将它们连接起来。List
是一个单子,因为这是可能的。
你可以编写一个函数map
,然后join
。此函数称为bind
、 或flatMap
、 或(>>=)
、 或(=<<)
。这通常是在 Haskell 中给出 monad 实例的方式。
monad 必须满足某些定律,即它join
必须是关联的。这意味着如果你有一个x
type的值[[[a]]]
thenjoin (join x)
应该 equal join (map join x)
。并且pure
必须是这样的一个join
身份join (pure x) == x
。
经过一番努力,我想我终于理解了单子。在重读了我自己对压倒性最高投票答案的冗长评论之后,我将提供这个解释。
要理解 monad,需要回答三个问题:
正如我在最初的评论中指出的那样,太多的单子解释被困在第 3 个问题中,而在没有真正充分涵盖问题 2 或问题 1 之前。
为什么需要一个单子?
像 Haskell 这样的纯函数式语言与像 C 或 Java 这样的命令式语言的不同之处在于,纯函数式程序不一定以特定的顺序执行,一次执行一个步骤。Haskell 程序更类似于数学函数,您可以在其中以任意数量的潜在顺序求解“方程”。这带来了许多好处,其中包括它消除了某些类型的错误的可能性,特别是那些与“状态”等相关的错误。
但是,使用这种编程风格解决某些问题并不那么简单。有些事情,比如控制台编程和文件 i/o,需要事情以特定的顺序发生,或者需要保持状态。处理这个问题的一种方法是创建一种表示计算状态的对象,以及一系列将状态对象作为输入的函数,并返回一个新的修改后的状态对象。
所以让我们创建一个假设的“状态”值,它代表控制台屏幕的状态。这个值是如何构造的并不重要,但是假设它是一个字节长度的 ascii 字符数组,表示当前在屏幕上可见的内容,以及一个表示用户输入的最后一行输入的数组,用伪代码表示。我们已经定义了一些函数来获取控制台状态、修改它并返回一个新的控制台状态。
consolestate MyConsole = new consolestate;
因此,要进行控制台编程,但以纯函数方式,您需要在彼此之间嵌套大量函数调用。
consolestate FinalConsole = print(input(print(myconsole, "Hello, what's your name?")),"hello, %inputbuffer%!");
以这种方式编程保持“纯”功能风格,同时强制对控制台的更改以特定顺序发生。但是,我们可能不希望像上面的例子那样一次只做几个操作。以这种方式嵌套函数将开始变得笨拙。我们想要的是与上面做基本相同的事情的代码,但写得更像这样:
consolestate FinalConsole = myconsole:
print("Hello, what's your name?"):
input():
print("hello, %inputbuffer%!");
这确实是一种更方便的编写方式。我们如何做到这一点?
什么是单子?
一旦您定义了一个类型(例如)以及一系列专门设计用于对该类型进行操作的函数,您就可以通过定义像(bind) 这样consolestate
的运算符自动将这些东西的整个包变成一个“monad”:
将左侧的返回值馈入右侧的函数参数,以及lift
将普通函数转换为与特定类型的绑定运算符一起使用的函数的运算符。
monad 是如何实现的?
查看其他答案,似乎可以自由地跳入其中的细节。
在几年前给出了这个问题的答案之后,我相信我可以改进和简化这个答案......
monad 是一种函数组合技术,它使用组合函数将某些输入场景的处理外部化bind
,以在组合过程中预处理输入。
在正常组合中,函数 ,compose (>>)
用于将组合函数按顺序应用于其前任的结果。重要的是,被组合的函数需要处理其输入的所有场景。
(x -> y) >> (y -> z)
这种设计可以通过重构输入来改进,以便更容易查询相关状态。因此,例如,如果包含有效性概念,则y
值可以变得Mb
例如,而不是简单地。(is_OK, b)
y
例如,当输入可能只是一个数字时,您可以将类型重新构造为bool
表示存在有效数字和元组中的数字的类型,而不是返回一个可以忠实地包含数字或不包含数字的字符串,例如,bool * float
。组合函数现在不再需要解析输入字符串来确定数字是否存在,而只需检查bool
元组的一部分。
(Ma -> Mb) >> (Mb -> Mc)
再次,组合自然发生compose
,因此每个函数必须单独处理其输入的所有场景,尽管现在这样做要容易得多。
但是,如果我们可以在处理场景是例行公事的时候将审讯工作外部化呢?例如,如果我们的程序在输入不正确时什么都不做,例如 when is_OK
is 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
函数可用。
因此,monad 是三样东西:
M
用于保存 monad 相关信息的外壳,bind
函数,以在将组合函数应用到它在 shell 中找到的内容值时利用此 shell 信息,以及a -> Mb
,产生包含一元管理数据的结果。一般来说,一个函数的输入比它的输出要严格得多,它可能包括诸如错误条件之类的东西;因此,Mb
结果结构通常非常有用。例如,除数为 时,除法运算符不返回数字0
。
此外,monad
s 可以包括包装函数,将值 , 包装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
monad 实际上是“类型运算符”的一种形式。它会做三件事。首先,它将一种类型的值“包装”(或以其他方式转换)为另一种类型(通常称为“一元类型”)。其次,它将使基础类型上可用的所有操作(或函数)在单子类型上可用。最后,它将为将其自身与另一个 monad 组合以产生复合 monad 提供支持。
“也许 monad”本质上等同于 Visual Basic / C# 中的“可空类型”。它采用不可为空的类型“T”并将其转换为“Nullable<T>”,然后定义所有二元运算符在 Nullable<T> 上的含义。
副作用也类似。创建了一个结构,其中包含对副作用的描述以及函数的返回值。当值在函数之间传递时,“提升”的操作会复制副作用。
它们被称为“monads”而不是“类型运算符”这个更容易理解的名称,原因如下:
(另见什么是单子?)
Monads 的一个很好的动机是 sigfpe (Dan Piponi) 的You could Have Invented Monads!(也许你已经有了)。还有很多其他的 monad 教程,其中许多错误地尝试使用各种类比以“简单的术语”来解释 monad:这是monad 教程的谬误;避免他们。
正如 DR MacIver 在告诉我们为什么您的语言很烂中所说:
所以,我讨厌 Haskell 的事情:
让我们从显而易见的开始。单子教程。不,不是单子。具体教程。它们是无穷无尽的,被夸大了,亲爱的上帝,它们是乏味的。此外,我从未见过任何令人信服的证据表明它们确实有帮助。阅读类定义,编写一些代码,克服可怕的名字。
你说你了解 Maybe 单子?很好,你在路上。刚开始使用其他单子,迟早你会明白单子是什么。
[如果你是数学导向的,你可能想忽略几十个教程并学习定义,或者跟随类别理论的讲座:) 定义的主要部分是 Monad M 涉及一个“类型构造函数”,它为每个定义现有类型“T”和新类型“M T”,以及在“常规”类型和“M”类型之间来回切换的一些方法。]
此外,令人惊讶的是,对 monad 的最佳介绍之一实际上是介绍 monad 的早期学术论文之一,即 Philip Wadler 的函数式编程的 Monads。与那里的许多人工教程不同,它实际上具有实用的、非平凡的激励示例。
单子是控制流的,就像抽象数据类型对于数据一样。
换句话说,许多开发人员对集合、列表、字典(或哈希或映射)和树的想法感到满意。在这些数据类型中有许多特殊情况(例如 InsertionOrderPreservingIdentityHashMap)。
然而,当面对程序“流程”时,许多开发人员并没有接触到比 if、switch/case、do、while、goto (grr) 和 (maybe) 闭包更多的构造。
因此,monad 只是一个控制流结构。替换 monad 的更好用语是“控制类型”。
因此,monad 有用于控制逻辑、语句或函数的槽——数据结构中的等价物就是说某些数据结构允许您添加和删除数据。
例如,“if”单子:
if( clause ) then block
最简单的有两个槽——一个子句和一个块。if
monad 通常用于评估子句的结果,如果不为假,则评估块。许多开发人员在学习 'if' 时并没有被介绍给 monad,而且没有必要了解 monad 来编写有效的逻辑。
Monad 可以变得更复杂,就像数据结构可以变得更复杂一样,但是有许多广义的 monad 类别可能具有相似的语义,但不同的实现和语法。
当然,与可以迭代或遍历数据结构的方式相同,可以评估 monad。
编译器可能支持也可能不支持用户定义的 monad。Haskell 确实如此。Ioke 有一些类似的功能,尽管在语言中没有使用术语 monad。
我最喜欢的 Monad 教程:
http://www.haskell.org/haskellwiki/All_About_Monads
(在 Google 搜索“monad tutorial”的 170,000 次点击中!)
@Stu:单子的重点是允许您(通常)将顺序语义添加到其他纯代码中;您甚至可以组合 monad(使用 Monad Transformers)并获得更有趣和更复杂的组合语义,例如使用错误处理、共享状态和日志记录进行解析。所有这一切都可以在纯代码中实现,monad 只是允许您将其抽象出来并在模块化库中重用它(在编程中总是很好),并提供方便的语法使其看起来很有必要。
Haskell 已经有运算符重载[1]:它使用类型类的方式与在 Java 或 C# 中使用接口的方式很相似,但 Haskell 恰好也允许像 + && 和 > 这样的非字母数字标记作为中缀标识符。如果您的意思是“重载分号”[2],那么它只是以您的方式查看它的运算符重载。这听起来像是黑魔法,并且自找麻烦“重载分号”(图片进取的 Perl 黑客听到了这个想法)但关键是没有 monad就没有分号,因为纯函数式代码不需要或允许显式排序。
这一切听起来比它需要的要复杂得多。sigfpe 的文章很酷,但使用 Haskell 来解释它,这有点未能打破先有鸡还是先有蛋的问题,即理解 Haskell 到 grok Monads 和理解 Monads 到 grok Haskell。
[1] 这是与 monad 不同的问题,但 monad 使用 Haskell 的运算符重载功能。
[2] 这也是一种过度简化,因为用于链接单子操作的运算符是 >>=(发音为“bind”),但有语法糖(“do”)允许您使用大括号和分号和/或缩进和换行符。
我对 monads 还是很陌生,但我想我会分享一个我觉得读起来感觉很好的链接(带图片!!): http: //www.matusiak.eu/numerodix/blog/2012/3/11/ monads-for-the-layman/ (无从属关系)
基本上,我从文章中得到的温暖而模糊的概念是,monad 基本上是允许不同函数以可组合方式工作的适配器,即能够将多个函数串起来并混合和匹配它们,而不必担心返回不一致类型等。因此,当我们尝试制作这些适配器时,BIND 函数负责将苹果与苹果、橙子与橙子放在一起。LIFT 函数负责获取“较低级别”的函数并“升级”它们以与 BIND 函数一起工作并且也是可组合的。
我希望我做对了,更重要的是,希望这篇文章对 monads 有一个有效的看法。如果不出意外,这篇文章帮助激发了我更多地了解 monad 的兴趣。
最近,我一直在以不同的方式思考 Monads。我一直认为它们以数学方式抽象出执行顺序,这使得新的多态性成为可能。
如果您使用的是命令式语言,并且您按顺序编写了一些表达式,则代码总是按该顺序运行。
在简单的情况下,当你使用 monad 时,感觉是一样的——你定义了一个按顺序发生的表达式列表。除此之外,根据您使用的 monad,您的代码可能会按顺序运行(如在 IO monad 中),一次在多个项目上并行运行(如在 List monad 中),它可能会在中途停止(如在 Maybe monad 中) ,它可能会在中途暂停以便稍后恢复(如在 Resumption monad 中),它可能会倒带并从头开始(如在 Transaction monad 中),或者它可能会在中途倒带以尝试其他选项(如在 Logic monad 中) .
而且由于 monad 是多态的,因此可以根据您的需要在不同的 monad 中运行相同的代码。
另外,在某些情况下,可以将 monad 组合在一起(使用 monad 转换器)以同时获得多个功能。
正如 Daniel Spiewak 解释的那样, Monads 不是隐喻,而是一种从常见模式中产生的实用抽象。
{-# 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
一元一元函数或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 应用程序 =<<
将 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 中,定义了身份单子
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 :
及其加法幺半群操作“追加”</p>
(++) :: [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)
_ =<< [] = []
扩展将由 Kleisli 箭头应用到元素的所有列表=<<
连接到单个结果列表中。++
[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 都是 functor,而且每个 applicative 都是 functor,每个 monad 也是 applicative。
使用 list 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
等效的单子理解,
[ 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
)
)
Do 表示法和 monad 理解是嵌套绑定表达式的语法糖。绑定运算符用于一元结果的本地名称绑定。
let x = v in e = (\ x -> e) $ v = v & (\ x -> e)
do { r <- m; c } = (\ r -> c) =<< m = m >>= (\ r -> c)
在哪里
(&) :: a -> (a -> b) -> b
(&) = flip ($)
infixl 0 &
定义保护函数
guard :: Additive m => Bool -> m ()
guard True = return ()
guard False = fail
其中单元类型或“空元组”</p>
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
应用于结果()
。如果为 false,则守卫会生成列表 monad 的fail
( []
),这不会产生要应用 Kleisli 箭头的结果>>
,因此p
将跳过此步骤。
臭名昭著的是,monad 被用来编码有状态的计算。
状态处理器是一个函数
forall st t. st -> (t, st)
转换状态st
并产生结果t
。状态 st
可以是任何东西。没有,旗帜,计数,阵列,手柄,机器,世界。
状态处理器的类型通常称为
type State st t = st -> (t, st)
状态处理器 monad 是 kinded * -> *
functor State st
。状态处理器单子的 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
状态访问由原语get
和,有状态单子put
的抽象方法提供:
{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Monad m => Stateful m st | m -> st where
get :: m st
put :: st -> m ()
m -> st
声明状态类型对 monad的函数依赖;例如,a将确定状态类型是唯一的。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
通常与记录字段访问器一起使用。
变量 threading 的 state 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)
其中X
,Y
是 中的对象C
。HomC(X, Y)
是中所有态射的同态类。函子必须保持态射恒等式和组合,即, in的“结构” 。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, _*>
内函子的
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
's 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
Monad 也可以不是 Kleislian 扩展定义,而是一种自然变换mu
,在编程中称为join
. 一个单子被定义mu
为一个类别的三元组C
,一个内函子
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
选项 monad的规范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
Philip Wadler:函数式编程的 Monad
Simon L Peyton Jones、Philip Wadler:命令式函数式编程
Jonathan MD Hill、Keith Clarke:范畴论、范畴论单子及其与函数式编程 的关系简介
Eugenio Moggi:计算和单子的概念
但是为什么一个如此抽象的理论对编程有任何用处呢?
答案很简单:作为计算机科学家,我们重视抽象!当我们设计软件组件的接口时,我们希望它尽可能少地透露实现。我们希望能够用许多替代方案、相同“概念”的许多其他“实例”来替换实现。当我们为很多程序库设计一个通用接口时,更重要的是我们选择的接口有多种实现。monad 概念的普遍性是我们如此看重的,因为范畴论是如此抽象,以至于它的概念对编程非常有用。
因此,我们在下面介绍的单子的概括也与范畴论有密切的联系也就不足为奇了。但我们强调,我们的目的是非常实用的:它不是“实现范畴论”,而是找到一种更通用的方式来构建组合子库。数学家已经为我们做了很多工作,这简直是我们的幸运!
从广义单子到箭头约翰休斯
除了上面的优秀答案之外,让我为您提供以下文章(Patrick Thomson 撰写)的链接,该文章通过将概念与 JavaScript 库jQuery(及其使用“方法链”操作 DOM 的方式)相关联来解释 monads : jQuery 是一个 Monad
jQuery 文档本身并没有提到术语“ monad”,而是谈到了可能更熟悉的“构建器模式”。这并没有改变你在那里有一个适当的 monad 的事实,甚至可能没有意识到这一点。
monad 是一种将共享共同上下文的计算组合在一起的方法。这就像建立一个管道网络。在构建网络时,没有数据流过它。但是,当我完成将所有位与“绑定”和“返回”拼凑在一起时,我会调用类似runMyMonad monad data
的东西,并且数据会通过管道流动。
在实践中,monad 是函数组合运算符的自定义实现,它负责处理副作用和不兼容的输入和返回值(用于链接)。
在了解那里时,对我帮助最大的两件事是:
Graham Hutton 的《 Haskell 编程》一书中的第 8 章“函数式解析器” 。实际上,这根本没有提到 monad,但是如果你能通读章节并真正理解其中的所有内容,特别是如何评估一系列绑定操作,你就会理解 monad 的内部结构。预计这需要多次尝试。
教程All About Monads。这提供了几个很好的例子来说明它们的使用,我不得不说我为我工作的附录中的类比。
在 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 的定义与范畴论map
中 Monad 的定义不同,后者由和轮流定义flatten
。尽管它们在某些映射下是等价的。这个演示文稿非常好:http ://www.slideshare.net/samthemonad/monad-presentation-scala-as-a-category
Monoid 似乎可以确保在 Monoid 上定义的所有操作和受支持的类型将始终在 Monoid 内返回受支持的类型。例如,任何数字 + 任何数字 = 一个数字,没有错误。
而除法接受两个小数,并返回一个小数,它将除以零定义为haskell中的无穷大(这恰好是一个小数)......
在任何情况下,Monads 似乎只是一种确保您的操作链以可预测的方式运行的方法,而声称为 Num -> Num 的函数与另一个用 x 调用的 Num->Num 函数组成的函数不会说,发射导弹。
另一方面,如果我们有一个发射导弹的函数,我们可以将它与也发射导弹的其他函数组合起来,因为我们的意图很明确——我们想发射导弹——但它不会尝试出于某种奇怪的原因打印“Hello World”。
在 Haskell 中,main 是 IO () 或 IO [()] 类型,区别很奇怪,我不会讨论它,但我认为会发生以下情况:
如果我有 main,我希望它执行一系列操作,我运行程序的原因是产生效果——通常是通过 IO. 因此,我可以在 main 中将 IO 操作链接在一起,以便 -- 只做 IO,仅此而已。
如果我尝试做一些不“返回 IO”的事情,程序会抱怨链不流动,或者基本上“这与我们正在尝试做的事情有什么关系——一个 IO 动作”,它似乎强制程序员要保持他们的思路,不要偏离并考虑发射导弹,同时创建排序算法——这不会流动。
基本上,Monads 似乎是给编译器的一个提示,“嘿,你知道这个函数在这里返回一个数字,它实际上并不总是工作,它有时可以产生一个数字,有时什么都没有,只要把它保存在头脑”。知道这一点,如果你尝试断言一个单子动作,单子动作可能会作为编译时异常说“嘿,这实际上不是一个数字,这可以是一个数字,但你不能假设这个,做点什么以确保流量是可接受的。” 这在一定程度上防止了不可预测的程序行为。
看来 monad 不是关于纯度,也不是控制,而是关于维护一个类别的身份,在该类别上所有行为都是可预测和定义的,或者不编译。当你被期望做某事时,你不能什么都不做,如果你被期望什么都不做(可见),你就不能做某事。
我能想到 Monads 的最大原因是——去看看 Procedural/OOP 代码,你会注意到你不知道程序从哪里开始,也不知道从哪里结束,你看到的只是很多跳跃和很多数学、魔法和导弹。您将无法维护它,并且如果可以,您将花费大量时间围绕整个程序进行思考,然后才能理解它的任何部分,因为这种情况下的模块化是基于相互依赖的“部分”代码,其中代码被优化为尽可能相关,以保证效率/相互关系。Monad 非常具体,并且根据定义进行了很好的定义,并确保程序流可以分析,并隔离难以分析的部分——因为它们本身就是 monad。单子似乎是“ 或者毁灭宇宙,甚至扭曲时间——我们不知道也不能保证它就是它本来的样子。一个 monad 保证它就是它本来的样子。这是非常强大的。或者毁灭宇宙,甚至扭曲时间——我们不知道也不能保证它就是它本来的样子。一个 monad 保证它就是它本来的样子。这是非常强大的。
“现实世界”中的所有事物似乎都是单子,因为它受到明确的可观察规律的约束,以防止混淆。这并不意味着我们必须模仿这个对象的所有操作来创建类,而是我们可以简单地说“正方形是正方形”,只是正方形,甚至不是矩形也不是圆形,“正方形有面积它的一个现有维度的长度乘以自身。无论你有什么正方形,如果它是二维空间中的正方形,它的面积绝对不能是它的长度的平方,这几乎是微不足道的证明。这非常强大,因为我们不需要做出断言来确保我们的世界是这样的,我们只是利用现实的含义来防止我们的程序偏离轨道。
我几乎肯定是错的,但我认为这可以帮助那里的人,所以希望它可以帮助别人。
这个答案从一个激励的例子开始,通过这个例子,推导出一个 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. ">
您不喜欢f
并g
负责将自己的日志消息附加到以前的日志信息中。(想象一下,为了论证,而不是附加字符串,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)
作“馈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
是 type t
,那么您的 pair 是“pair of t
and string”类型的值。调用那个类型M t
。
M
是一个类型构造函数:M
alone 不是指一个类型,而是M _
在你填空后指一个类型。AnM int
是一对 int 和一个字符串。AnM string
是一对字符串和字符串。等等。
恭喜,你已经创建了一个 monad!
形式上,你的 monad 是 tuple <M, feed, wrap>
。
monad 是一个元组<M, feed, wrap>
,其中:
M
是一个类型构造函数。feed
t
接受一个(接受一个并返回一个的函数M u
)和一个M t
并返回一个M u
。wrap
接受一个v
并返回一个M v
。t
, u
, 和v
是任何三种类型,可能相同也可能不同。monad 满足您为特定 monad 证明的三个属性:
将包装t
的内容提供给函数与将展开的内容传递t
给函数相同。
正式地:feed(f, wrap(x)) = f(x)
喂入 inM t
对.wrap
没有任何作用M t
。
正式地:feed(wrap, m) = m
将M t
(调用它m
)输入到一个函数中
t
_g
M u
_ n
_g
n
_f
是相同的
m
_g
n
_g
n
_f
正式地:feed(h, m) = feed(f, feed(g, m))
哪里h(x) := feed(f, g(x))
通常,feed
称为bind
(>>=
在 Haskell 中也称为)并wrap
称为return
.
我将尝试Monad
在 Haskell 的上下文中进行解释。
在函数式编程中,函数组合很重要。它允许我们的程序由小而易读的函数组成。
假设我们有两个函数:g :: Int -> String
和f :: String -> Bool
。
我们可以做(f . g) x
,这与 一样f (g x)
,其中x
是一个Int
值。
在进行组合/将一个函数的结果应用于另一个函数时,类型匹配很重要。在上述情况下,返回结果的类型g
必须与 接受的类型相同f
。
但有时值在上下文中,这使得排列类型变得不那么容易。(在上下文中具有值非常有用。例如,Maybe Int
类型表示Int
可能不存在IO String
的值,类型表示String
由于执行某些副作用而存在的值。)
假设我们现在有g1 :: Int -> Maybe String
和f1 :: String -> Maybe Bool
。g1
和分别f1
非常相似。g
f
我们不能做(f1 . g1) x
or f1 (g1 x)
, wherex
是一个Int
值。返回的结果类型g1
不是f1
预期的。
我们可以用操作符组合f
和,但现在我们不能用和组合。问题是我们不能直接将上下文中的值传递给期望不在上下文中的值的函数。g
.
f1
g1
.
如果我们引入一个操作符来 compose g1
and f1
,这样我们可以写,那不是很好(f1 OPERATOR g1) x
吗?g1
在上下文中返回一个值。该值将脱离上下文并应用于f1
. 是的,我们有这样的运营商。是<=<
。
我们也>>=
有为我们做完全相同的事情的运算符,尽管语法略有不同。
我们写:g1 x >>= f1
。g1 x
是一个Maybe Int
值。>>=
运算符帮助将该值Int
从“可能不存在”的上下文中取出,并将其应用于f1
. 的结果f1
,即 a Maybe Bool
,将是整个>>=
运算的结果。
最后,为什么Monad
有用?因为Monad
是定义运算符的类型类,与定义and运算符的类型类>>=
非常相似。Eq
==
/=
总而言之,Monad
类型类定义了>>=
允许我们将上下文中的值(我们称之为单子值)传递给不期望上下文中的值的函数的运算符。上下文将得到处理。
如果这里要记住一件事,那就是Monad
s 允许函数组合涉及上下文中的值。
http://code.google.com/p/monad-tutorial/是一项正在进行的工作,以准确解决这个问题。
如果我理解正确的话,IEnumerable 是从 monads 派生的。我想知道对于我们这些来自 C# 世界的人来说,这是否是一个有趣的方法?
对于它的价值,这里有一些对我有帮助的教程链接(不,我仍然不明白什么是单子)。
世界需要的是另一篇 monad 博客文章,但我认为这对于识别野外现有的 monad 很有用。
上面是一个叫做谢尔宾斯基三角形的分形,这是我唯一记得要画的分形。分形是类似于上述三角形的自相似结构,其中部分与整体相似(在这种情况下,正好是父三角形的一半)。
单子是分形。给定一个单子数据结构,它的值可以组合形成数据结构的另一个值。这就是它对编程有用的原因,也是它在许多情况下发生的原因。
一个非常简单的答案是:
Monad 是一种抽象,它为封装值、计算新封装值和解包封装值提供接口。
它们在实践中的方便之处在于它们提供了一个统一的接口来创建模型状态而不是有状态的数据类型。
重要的是要理解 Monad 是一种抽象,即用于处理某种数据结构的抽象接口。然后该接口用于构建具有单子行为的数据类型。
您可以在 Ruby 中的 Monads, Part 1: Introduction中找到非常好的和实用的介绍。
请参阅以下幻灯片,尝试一次从一个角度回答这个问题,重点是 Scala:
让下面的“ {| 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
知道,monad 和>>=
他们在勾结。
(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}
因此,如果它确实包含它所宣传的,那么 Maybe monad 让计算继续a
,但如果它不包含则中止计算。然而,结果仍然是一段单子数据,尽管不是f
. 出于这个原因,Maybe monad 被用来表示失败的上下文。
不同的 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
monad 实际上没有,a
但它认识一个人并会a
为你得到那个。State st
monad 有一个秘密藏匿点,st
它将传递到f
桌子底下,即使f
只是来要求一个a
. monad与Reader r
类似State st
,尽管它只让我们f
看一下r
。
所有这一切的重点是,任何类型的数据被声明为 Monad 都在声明某种从 monad 中提取值的上下文。这一切的大收获?好吧,它很容易用某种上下文来进行计算。然而,当将多个上下文负载计算串在一起时,它可能会变得混乱。monad 操作负责解决上下文的交互,因此程序员不必这样做。
请注意,>>=
通过从f
. 也就是说,在上面的例子中Nothing
,f
不再决定在Nothing
; 的情况下做什么。它被编码在>>=
. 这是权衡。如果有必要f
在 的情况下决定要做什么Nothing
,那么f
应该是一个函数 from Maybe a
to Maybe b
。在这种情况下,Maybe
作为一个 monad 是无关紧要的。
但是请注意,有时数据类型不会导出它的构造函数(看看你的 IO),如果我们想要使用广告值,我们别无选择,只能使用它的 monadic 接口。
AMonad
是一个Applicative
(即你可以提升二进制的东西 - 因此,“ n -ary” - 函数到,(1)并将纯值注入(2))Functor
(即你可以映射的东西,(3)即将一元函数提升到(3)),并增加了扁平化嵌套数据类型的能力(三个概念中的每一个都遵循其相应的一组定律)。在 Haskell 中,这种展平操作称为join
.
此“ ”操作的一般(通用,参数)类型是:join
join :: Monad m => m (m a) -> m a
对于任何 monad m
(NBm
类型中的所有 s 都是相同的!)。
一个特定的m
monad 定义了它的特定版本,适用于由 type 的 monadic 值“携带”的join
任何值类型。一些特定的类型是:a
m a
join :: [[a]] -> [a] -- for lists, or nondeterministic values
join :: Maybe (Maybe a) -> Maybe a -- for Maybe, or optional values
join :: IO (IO a) -> IO a -- for I/O-produced values
该join
操作将m
产生- 类型值的-m
计算的 - 计算转换为a
- 类型值的组合-m
计算a
。这允许将计算步骤组合成一个更大的计算。
这个计算步骤- 结合“绑定”(>>=
)运算符简单地使用fmap
和join
一起,即
(ma >>= k) == join (fmap k ma)
{-
ma :: m a -- `m`-computation which produces `a`-type values
k :: a -> m b -- create new `m`-computation from an `a`-type value
fmap k ma :: m ( m b ) -- `m`-computation of `m`-computation of `b`-type values
(m >>= k) :: m b -- `m`-computation which produces `b`-type values
-}
相反,join
可以通过 bind 定义,join mma == join (fmap id mma) == mma >>= id
其中id ma = ma
-- 对给定类型更方便m
。
对于 monad,do
-notation 及其等效的bind -using代码,
do { x <- mx ; y <- my ; return (f x y) } -- x :: a , mx :: m a
-- y :: b , my :: m b
mx >>= (\x -> -- nested
my >>= (\y -> -- lambda
return (f x y) )) -- functions
可以读作
首先 "do"
mx
,当它完成后,得到它的 "result"x
并让我用它来 "do" 别的事情。
在给定的do
块中,绑定箭头右侧的每个值<-
都是m a
某种类型的类型,并且在整个块a
中都是相同的 monad 。m
do
return x
是一个中性m
的计算,它只产生x
它给定的纯值,因此绑定任何m
计算return
根本不会改变计算。
(1)与liftA2 :: Applicative m => (a -> b -> c) -> m a -> m b -> m c
(2)与pure :: Applicative m => a -> m a
(3)与fmap :: Functor m => (a -> b) -> m a -> m b
还有等效的 Monad 方法,
liftM2 :: Monad m => (a -> b -> c) -> m a -> m b -> m c
return :: Monad m => a -> m a
liftM :: Monad m => (a -> b) -> m a -> m b
给定一个单子,其他定义可以如下
pure a = return a
fmap f ma = do { a <- ma ; return (f a) }
liftA2 f ma mb = do { a <- ma ; b <- mb ; return (f a b) }
(ma >>= k) = do { a <- ma ; b <- k a ; return b }
monad 是用来封装具有变化状态的对象的东西。它最常出现在不允许您具有可修改状态的语言中(例如,Haskell)。
一个例子是文件 I/O。
您将能够使用文件 I/O 的 monad 将不断变化的状态性质隔离到仅使用 Monad 的代码。Monad 内部的代码可以有效地忽略 Monad 外部世界的变化状态——这使得推理程序的整体效果变得更加容易。
Princess对F# Computation Expressions的解释帮助了我,虽然我仍然不能说我真的明白了。
编辑:这个系列 - 用 javascript 解释 monads - 是对我来说“打破平衡”的系列。
http://blog.jcoglan.com/2011/03/06/monad-syntax-for-javascript/
http://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of-asynchronous-programming/
我认为理解 monad 是一件让你毛骨悚然的事情。从这个意义上说,尽可能多地阅读“教程”是一个好主意,但通常奇怪的东西(不熟悉的语言或语法)会阻止你的大脑专注于基本内容。
我难以理解的一些事情:
a -> M<a>
) 和 Bind( M<a> -> (a -> M<b>) -> M<b>
) 很棒,但我永远无法理解的是 Bind 如何提取a
fromM<a>
以便将其传递给a -> M<b>
. 我想我从来没有在任何地方读过(也许其他人都很明显), Return ( M<a> -> a
)的反面必须存在于monad 中,它只是不需要暴露。我也在尝试理解单子。这是我的版本:
单子是关于对重复的事物进行抽象。首先,monad 本身是一个类型化的接口(就像一个抽象的泛型类),它有两个函数:绑定和返回具有定义的签名。然后,我们可以基于该抽象 monad 创建具体的 monad,当然还有特定的 bind 和 return 实现。此外,bind 和 return 必须满足一些不变量,以便可以组合/链接具体的 monad。
当我们有接口、类型、类和其他工具来创建抽象时,为什么要创建 monad 概念?因为 monad 提供了更多:它们以一种无需任何样板就可以组合数据的方式强制重新思考问题。
解释 monad 似乎就像解释控制流语句一样。想象一下,一个非程序员要求你解释它们?
你可以给他们一个涉及理论的解释——布尔逻辑、寄存器值、指针、堆栈和帧。但这太疯狂了。
你可以用语法来解释它们。基本上,C 中的所有控制流语句都有花括号,您可以通过它们相对于括号的位置来区分条件和条件代码。那可能更疯狂。
或者,您还可以解释循环、if 语句、例程、子例程以及可能的协同例程。
Monads 可以替代相当多的编程技术。支持它们的语言中有特定的语法,以及一些关于它们的理论。
它们也是函数式程序员使用命令式代码而不实际承认它的一种方式,但这不是他们唯一的用途。
本质上,实际上,单子允许回调嵌套
(具有相互递归线程状态(请原谅连字符))
(以可组合(或可分解)的方式)
(具有类型安全性(有时(取决于语言)))
)) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ))))))))))))))))))))))))))))))))))))))))
EG 这不是一个单子:
//JavaScript is 'Practical'
var getAllThree =
bind(getFirst, function(first){
return bind(getSecond,function(second){
return bind(getThird, function(third){
var fancyResult = // And now make do fancy
// with first, second,
// and third
return RETURN(fancyResult);
});});});
但是 monads 启用了这样的代码。
monad 实际上是一组类型:
{bind,RETURN,maybe others I don't know...}
.
这本质上是无关紧要的,实际上是不切实际的。
所以现在我可以使用它了:
var fancyResultReferenceOutsideOfMonad =
getAllThree(someKindOfInputAcceptableToOurGetFunctionsButProbablyAString);
//Ignore this please, throwing away types, yay JavaScript:
// RETURN = K
// bind = \getterFn,cb ->
// \in -> let(result,newState) = getterFn(in) in cb(result)(newState)
或分解:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(fancyResult2){
return bind(getThird, function(third){
var fancyResult3 = // And now make do fancy
// with fancyResult2,
// and third
return RETURN(fancyResult3);
});});
或忽略某些结果:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(____dontCare____NotGonnaUse____){
return bind(getThird, function(three){
var fancyResult3 = // And now make do fancy
// with `three` only!
return RETURN(fancyResult3);
});});
或者简化一个简单的案例:
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(_){
return bind(getThird, function(three){
return RETURN(three);
});});
至(使用“正确身份”):
var getFirstTwo =
bind(getFirst, function(first){
return bind(getSecond,function(second){
var fancyResult2 = // And now make do fancy
// with first and second
return RETURN(fancyResult2);
});})
, getAllThree =
bind(getFirstTwo, function(_){
return getThird;
});
或者将它们重新组合在一起:
var getAllThree =
bind(getFirst, function(first_dontCareNow){
return bind(getSecond,function(second_dontCareNow){
return getThird;
});});
在您尝试解决
诸如解析或模块/ajax/资源加载等真正混乱的问题之前,这些能力的实用性并没有真正出现或变得清晰。
你能想象成千上万行 indexOf/subString 的逻辑吗?
如果频繁的解析步骤包含在小函数中怎么办?, , , 或?
等函数
如果这些函数在回调中为您提供结果,
而不必与 Regex 组和 arguments.slice 混淆怎么办?
如果他们的组成/分解被很好地理解怎么办?
这样您就可以自下而上构建大型解析器? chars
spaces
upperChars
digits
因此,管理嵌套回调范围的能力非常实用,
尤其是在使用单子解析器组合库时。
(也就是说,根据我的经验)
不要挂断:
- 类别理论
- 可能是单子
- 单子定律
- 哈斯克尔
- !!!!
monad 是一个容器,但用于数据。一个特殊的容器。
所有容器都可以有开口、把手和喷口,但这些容器都保证有一定的开口、把手和喷口。
为什么?因为这些有保证的开口、把手和喷口对于以特定、常见的方式拾取和连接容器很有用。
这使您可以拿起不同的容器,而不必对它们有太多了解。它还允许不同类型的容器轻松链接在一起。
map
仅使用 Python 列表和函数来解释 monad 的另一种尝试。我完全接受这不是一个完整的解释,但我希望它能够触及核心概念。
我从有关 Monads 的funfunfunction 视频和 Learn You A Haskell 章节“For a Few Monads More”中获得了这个基础。我强烈推荐观看 funfunfunction 视频。
简单来说,Monad 是具有 amap
和flatMap
函数的对象(bind
在 Haskell 中)。有一些额外的必需属性,但这些是核心属性。
flatMap
“扁平化”地图的输出,对于列表,这只是连接列表的值,例如
concat([[1], [4], [9]]) = [1, 4, 9]
所以在 Python 中,我们基本上可以用这两个函数实现一个 Monad:
def flatMap(func, lst):
return concat(map(func, lst))
def concat(lst):
return sum(lst, [])
func
是任何接受一个值并返回一个列表的函数,例如
lambda x: [x*x]
为清楚起见,我通过一个简单concat
的函数在 Python 中创建了该函数,该函数对列表求和(Haskell 有一个本地方法)。[] + [1] + [4] + [9] = [1, 4, 9]
concat
我假设您知道该map
功能是什么,例如:
>>> list(map(lambda x: [x*x], [1,2,3]))
[[1], [4], [9]]
扁平化是 Monad 的关键概念,对于每个作为 Monad 的对象,这种扁平化允许您获得包裹在 Monad 中的值。
现在我们可以调用:
>>> flatMap(lambda x: [x*x], [1,2,3])
[1, 4, 9]
这个 lambda 取值 x 并将其放入列表中。monad 适用于从值到 monad 类型的任何函数,因此在这种情况下是列表。
那是你的 monad 定义的。
我认为它们为什么有用的问题已经在其他问题中得到了回答。
其他不是列表的例子是 JavaScript Promises,它有then
方法和 JavaScript Streams,它有一个flatMap
方法。
所以 Promises 和 Streams 使用了一个稍微不同的函数,它将 Stream 或 Promise 展平并从内部返回值。
Haskell 列表单子具有以下定义:
instance Monad [] where
return x = [x]
xs >>= f = concat (map f xs)
fail _ = []
即有三个函数return
(不要与大多数其他语言中的 return 混淆),>>=
(the flatMap
) 和fail
.
希望您能看到以下之间的相似之处:
xs >>= f = concat (map f xs)
和:
def flatMap(f, xs):
return concat(map(f, xs))
http://mikehadlow.blogspot.com/2011/02/monads-in-c-8-video-of-my-ddd9-monad.html
这是您正在寻找的视频。
在 C# 中演示组合和对齐类型的问题,然后在 C# 中正确实现它们。最后,他展示了相同的 C# 代码在 F# 和最后在 Haskell 中的外观。
数学思维
简而言之:一种用于组合计算的代数结构。
return data
:创建一个仅在 monad 世界中生成数据的计算。
(return data) >>= (return func)
:第二个参数接受第一个参数作为数据生成器并创建一个连接它们的新计算。
您可以认为(>>=)和return本身不会进行任何计算。他们只是简单地组合并创建计算。
当且仅当main触发它时,才会计算任何 monad 计算。
在 Coursera “响应式编程原理”培训中 - Erik Meier 将它们描述为:
"Monads are return types that guide you through the happy path." -Erik Meijer
根据我们在谈论单子时所谈论的内容,“什么是单子”这个问题是错误的:
对“什么是单子?”这个问题的简短回答 是它是内函子类别中的一个幺半群,还是它是一种通用数据类型,配备了满足某些定律的两个操作。这是正确的,但它并没有揭示一个重要的更大的图景。这是因为问题是错误的。在本文中,我们旨在回答正确的问题,即“作者在谈论 monad 时真正说的是什么?”
虽然那篇论文没有直接回答什么是 monad,但它有助于理解不同背景的人在谈论 monad 时的含义以及原因。
Monad 是一个带有特殊机器的盒子,它允许你用两个嵌套的盒子制作一个普通的盒子——但仍然保留两个盒子的一些形状。
具体来说,它允许您执行join
, 类型Monad m => m (m a) -> m a
。
它还需要一个return
动作,它只是包装一个值。return :: Monad m => a -> m a
你也可以说join
unboxes 和return
wraps - 但join
不是类型(它不会解开所有的 Monad,它会解开 Monad 里面有 Monad。)Monad m => m a -> a
所以它需要一个 Monad 盒子 ( Monad m =>
, m
),里面有一个盒子 ( (m a)
) 并制作一个普通盒子 ( m a
)。
但是,Monad 通常用于(>>=)
(口语“绑定”)运算符,它本质上是fmap
彼此join
紧接的。具体来说,
x >>= f = join (fmap f x)
(>>=) :: Monad m => (a -> m b) -> m a -> m b
请注意,该函数来自第二个参数,而不是fmap
.
另外,join = (>>= id)
.
现在为什么这很有用?本质上,它允许您制作将动作串在一起的程序,同时在某种框架(Monad)中工作。
在 Haskell 中,Monad 最突出的用途是IO
Monad。
现在,是在 Haskell 中对ActionIO
进行分类的类型。在这里,Monad 系统是保存(花哨的大词)的唯一方法:
本质上,一个 IO 动作如getLine :: IO String
不能用字符串代替,因为它总是有不同的类型。可以将IO
其视为一种将东西传送给您的神奇盒子。
但是,仍然只是这么说,getLine :: IO String
并且所有功能都接受IO a
导致混乱,因为可能不需要这些功能。会const "üp§" getLine
做什么?(const
丢弃第二个参数。。const a b = a
)getLine
不需要评估,但它应该做 IO!这使得行为相当不可预测 - 并且也使类型系统不那么“纯粹”,因为所有函数都会采用a
和IO a
值。
进入IO
单子。
要将动作串在一起,您只需将嵌套动作展平即可。
并且要将函数应用于 IO 操作的输出,a
在IO a
类型中,您只需使用(>>=)
.
例如,输出输入的行(输出行是一个产生 IO 动作的函数,匹配 的右参数>>=
):
getLine >>= putStrLn :: IO ()
-- putStrLn :: String -> IO ()
这可以用do
环境更直观地编写:
do line <- getLine
putStrLn line
本质上,do
像这样的块:
do x <- a
y <- b
z <- f x y
w <- g z
h x
k <- h z
l k w
... 变成这样:
a >>= \x ->
b >>= \y ->
f x y >>= \z ->
g z >>= \w ->
h x >>= \_ ->
h z >>= \k ->
l k w
还有>>
操作符for m >>= \_ -> f
(当不需要盒子里的值来制作盒子里的新盒子时)也可以写成a >> b = a >>= const b
(const a b = a
)
此外,return
操作符是根据 IO 直觉建模的——它返回一个具有最小上下文的值,在这种情况下没有 IO。由于a
inIO a
代表返回的类型,这类似于return(a)
命令式编程语言中的内容 - 但它不会停止动作链!f >>= return >>= g
是一样的f >>= g
。仅当您返回的术语已在链中较早创建时才有用 - 见上文。
当然,还有其他的 Monad,否则它就不会被称为 Monad,它会被称为“IO Control”之类的东西。
例如,List Monad ( Monad []
) 通过连接变平 - 使(>>=)
运算符对列表的所有元素执行函数。这可以看作是“不确定性”,其中 List 是许多可能的值,而 Monad 框架正在制作所有可能的组合。
例如(在 GHCi 中):
Prelude> [1, 2, 3] >>= replicate 3 -- Simple binding
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> concat (map (replicate 3) [1, 2, 3]) -- Same operation, more explicit
[1, 1, 1, 2, 2, 2, 3, 3, 3]
Prelude> [1, 2, 3] >> "uq"
"uququq"
Prelude> return 2 :: [Int]
[2]
Prelude> join [[1, 2], [3, 4]]
[1, 2, 3, 4]
因为:
join a = concat a
a >>= f = join (fmap f a)
return a = [a] -- or "= (:[])"
Nothing
如果发生这种情况,Maybe Monad 只会使所有结果无效。也就是说,绑定会自动检查函数 ( a >>=
f
) 是否返回或值 ( a
>>= f
) 是否为Nothing
-,然后Nothing
也返回。
join Nothing = Nothing
join (Just Nothing) = Nothing
join (Just x) = x
a >>= f = join (fmap f a)
或者,更明确地说:
Nothing >>= _ = Nothing
(Just x) >>= f = f x
State Monad 适用于也修改某些共享状态的函数 -s -> (a, s)
所以 的参数>>=
是:: a -> s -> (a, s)
。
这个名字有点用词不当,因为State
它确实是用于状态修改功能,而不是用于状态 - 状态本身确实没有有趣的属性,它只是被改变了。
例如:
pop :: [a] -> (a , [a])
pop (h:t) = (h, t)
sPop = state pop -- The module for State exports no State constructor,
-- only a state function
push :: a -> [a] -> ((), [a])
push x l = ((), x : l)
sPush = state push
swap = do a <- sPop
b <- sPop
sPush a
sPush b
get2 = do a <- sPop
b <- sPop
return (a, b)
getswapped = do swap
get2
然后:
Main*> runState swap [1, 2, 3]
((), [2, 1, 3])
Main*> runState get2 [1, 2, 3]
((1, 2), [1, 2, 3]
Main*> runState (swap >> get2) [1, 2, 3]
((2, 1), [2, 1, 3])
Main*> runState getswapped [1, 2, 3]
((2, 1), [2, 1, 3])
还:
Prelude> runState (return 0) 1
(0, 1)
如果您要求对如此抽象的事物进行简洁实用的解释,那么您只能希望得到一个抽象的答案:
a -> b
是表示从a
s 到b
s 的计算的一种方式。您可以链接计算,也就是将它们组合在一起:
(b -> c) -> (a -> b) -> (a -> c)
更复杂的计算需要更复杂的类型,例如:
a -> f b
是从a
s 到b
s的计算类型f
。您还可以编写它们:
(b -> f c) -> (a -> f b) -> (a -> f c)
事实证明,这种模式几乎无处不在,并且与上面的第一个组合具有相同的属性(关联性、左右身份)。
必须给这个模式一个名字,但是知道第一个组合被正式表征为Semigroupoid会有所帮助吗?
“单子和括号一样有趣和重要” (Oleg Kiselyov)
当用 C#/Java 术语解释时,这很简单:
monad 是一个接受参数并返回特殊类型的函数。
这个 monad 返回的特殊类型也称为 monad。(单子是#1 和#2 的组合)
有一些语法糖可以使调用这个函数和类型转换更容易。
monad 有助于使函数式程序员的生活更轻松。典型的例子:Maybe
monad 有两个参数,一个值和一个函数。null
如果传递的值为 ,则返回null
。否则,它会评估函数。如果我们需要一个特殊的返回类型,我们也会调用这个返回类型Maybe
。一个非常粗略的实现如下所示:
object Maybe(object value, Func<object,object> function)
{
if(value==null)
return null;
return function(value);
}
这在 C# 中非常无用,因为这种语言缺乏使 monad 有用所需的语法糖。但是 monad 允许你用函数式编程语言编写更简洁的代码。
程序员经常在链中调用 monad,如下所示:
var x = Maybe(x, x2 => Maybe(y, y2 => Add(x2, y2)));
在此示例中,仅当和都是非Add
时才调用该方法,否则将返回。x
y
null
null
回答最初的问题:monad 是一个函数和一个类型。就像一个特殊的interface
.
遵循您简短、简洁、实用的指示:
理解 monad 的最简单方法是在上下文中应用/组合函数。假设您有两个计算,它们都可以看作是两个数学函数f
,并且g
.
f
接受一个字符串并产生另一个字符串(取前两个字母)g
接受一个字符串并产生另一个字符串(大写转换)因此,在任何语言中,“取前两个字母并将它们转换为大写”的转换将被写成 g(f("some string"))。所以,在纯完美函数的世界里,组合就是:做一件事,然后做另一件事。
但是假设我们生活在一个可能会失败的函数世界中。例如:输入字符串可能是一个字符长,所以 f 会失败。所以在这种情况下
f
接受一个字符串并产生一个字符串或什么都没有。g
仅当 f 没有失败时才生成字符串。否则,什么都不会产生所以现在, g(f("some string")) 需要一些额外的检查:“计算f
,如果失败则g
应该返回 Nothing,否则计算 g”
这个想法可以应用于任何参数化类型,如下所示:
让 Context[Sometype] 是Sometype在Context中的计算。考虑功能
f:: AnyType -> Context[Sometype]
g:: Sometype -> Context[AnyOtherType]
组合 g(f()) 应该读作“计算 f。在这个上下文中做一些额外的计算,然后计算 g 如果它在上下文中有意义”