有人能解释一下Monad吗?

16 浏览
0 Comments

有人能解释一下Monad吗?

最近简单地了解了Haskell,对于monad到底是什么,有没有一个简洁、明了、实用的解释呢?\n我发现大多数解释都比较晦涩难懂,缺乏实际细节。

0
0 Comments

什么是Monad?简单来说,它是一种特定的操作链接方式。实际上,你在编写执行步骤,并使用“bind函数”将它们链接在一起。这个bind函数的作用就像一个分号,它将一个过程中的步骤分隔开来。bind函数的工作是将上一个步骤的输出传递给下一个步骤。

但是,有不止一种类型的Monad。为什么?每个有用的Monad除了作为Monad外,还有其他特殊功能。每个有用的Monad都有一个独特的“特殊能力”。

基本上,每个Monad都有自己的bind函数实现。你可以编写一个bind函数,使它在执行步骤之间执行一些特殊的操作。例如:

- 如果每个步骤返回一个成功/失败指示器,你可以让bind函数只在前一个步骤成功时才执行下一个步骤。这样,一个失败的步骤会“自动”中止整个序列,无需进行任何条件测试。(Failure Monad)

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

- 你可以使每个步骤返回多个结果,并让bind函数循环处理它们,将每个结果传递给下一个步骤。这样,在处理多个结果时,你就不必到处写循环了,bind函数会自动为你完成所有这些。(List Monad)

- 除了将“结果”从一个步骤传递到另一个步骤,你还可以让bind函数传递额外的数据。这样,这些数据就不会出现在你的源代码中,但你仍然可以从任何地方访问它,无需手动将其传递给每个函数。(Reader Monad)

- 你可以使“额外数据”可以被替换。这样一来,你就可以模拟破坏性更新,而不实际进行破坏性更新。(State Monad及其相关的Writer Monad)

- 由于你只是“模拟”破坏性更新,所以你可以轻松地做一些使用“真实”的破坏性更新无法实现的事情。例如,你可以“撤销最后一次更新”,或者“恢复到旧版本”。

- 你可以创建一个可以“暂停”计算的Monad,这样你就可以暂停程序,进入并调整内部状态数据,然后再恢复它。

- 你可以将“continuations”实现为一个Monad。这让你能够“打破人们的思维!”

Monad能够实现以上所有功能以及更多。当然,所有这些功能在没有Monad的情况下也是完全可能的。只是使用Monad会更加容易。

Haskell至少支持这一点,从数学上讲,你可以以>>=和return的形式或join和ap的形式定义一个Monad。>>=和return是让Monad实际上有用的东西,但join和ap可以给出对Monad是什么的更直观的理解。

Monad是一种特定的操作链接方式,它使操作更加简单易懂。它为每个步骤提供了灵活的处理方式,并允许实现各种有用的功能。虽然在没有Monad的情况下也可以实现相同的功能,但使用Monad会更加方便。

0
0 Comments

什么是单子?这个问题的出现是因为单子这个术语对于非数学家来说有点空洞。另一个术语是“计算生成器”,这个术语更能描述单子的实际用途。它们是一种用于链接操作的模式。它看起来有点像面向对象语言中的方法链接,但机制略有不同。该模式主要用于函数式语言(特别是广泛使用单子的Haskell),但可以用于任何支持高阶函数的语言(即能够接受其他函数作为参数的函数)。JavaScript中的数组支持该模式,让我们以此为例进行说明。

模式的要点是,我们有一个类型(在这种情况下是Array),该类型有一个以函数作为参数的方法。提供的操作必须返回相同类型的实例(即返回一个Array)。首先是一个不使用单子模式的方法链接的示例:

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

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

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

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

(像map或flatMap这样的方法提供的函数参数在JavaScript中通常被称为“回调函数”。我将其称为“操作”,因为它更加通用。)

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

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

结果是数组[2,4]。以单子形式链接相同的操作:

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

得到相同的结果,即数组[2,4]。

您会立即注意到,单子形式比非单子形式要复杂得多!这只是表明单子不一定是“好”的。它们是一种有时有益有时无益的模式。

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

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

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

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

这可以用TypeScript表示得更正式一些。数组具有类型Array,其中T是数组中元素的类型。方法flatMap()接受类型为T => Array的函数参数,并返回一个Array

在更通用的情况下,单子是任何类型Foo,该类型具有“bind”方法,该方法接受类型为Bar => Foo的函数参数,并返回一个Foo

这回答了“单子是什么”。本答案的其余部分将试图通过示例解释为什么单子可以成为像Haskell这样的语言中有用的模式。

哈斯克尔和Do-notation

为了直接将map/filter示例翻译成Haskell,我们将flatMap替换为>>=运算符:

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

>>=运算符是Haskell中的bind函数。当操作数是列表时,它执行与JavaScript中flatMap相同的操作,但对于其他类型,它具有不同的含义。

但是Haskell还有一个专用的语法用于单子表达式,即do块,它完全隐藏了bind运算符:

do

a <- [1,2,3]

b <- [a+1]

if b == 3 then [] else [b]

这隐藏了“连接”问题,并让您专注于每个步骤中应用的实际操作。

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

反斜杠<-看起来很像赋值,但请注意,这是传递给bind的参数。因此,当右侧的表达式是整数列表时,左侧的变量将是单个整数,但将为列表中的每个整数执行。

例子:安全导航(Maybe类型)

够了关于列表的内容,让我们看看单子模式如何适用于其他类型。

某些函数可能不总是返回有效值。在Haskell中,这由Maybe类型表示,它是一个选项,可以是Just值,也可以是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通过Maybe的单子模式来解决这个问题:

do

user <- getUser 17

addr <- getAddress user

getStreetName addr

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

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

请注意,正是单子模式和Haskell中对单子友好的语法的组合才导致了更简洁的代码。在没有任何特殊语法支持单子的语言(比如JavaScript)中,我怀疑单子模式能够在这种情况下简化代码。

可变状态

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

add2 :: State Integer Integer

add2 = do

-- 将状态加1

x <- get

put (x + 1)

-- 以另一种方式进行增加

modify (+1)

-- 返回状态

get

evalState add2 7

=> 9

add2函数构建了一个单子链,然后使用7作为初始状态进行评估。

显然,这只对Haskell有意义。其他语言直接支持可变状态。Haskell通常是按需启用语言功能的,“需要时启用可变状态,并且类型系统保证效果是显式的。IO就是另一个例子。

IO

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

与任何其他实用语言一样,Haskell具有许多内置函数与外部世界进行交互:putStrLine、readLine等等。这些函数被称为“不纯”,因为它们会引起副作用或具有非确定性结果。即使是获取时间这样简单的事情也被认为是不纯的,因为结果是非确定性的-使用相同的参数调用它两次可能返回不同的值。

纯函数是确定性的-其结果仅取决于传递的参数,并且对环境没有任何副作用,只返回一个值。

Haskell非常鼓励使用纯函数-这是该语言的一个重要卖点。对于纯粹主义者来说,你需要一些不纯的函数才能做到实用。Haskell的妥协是清晰地区分纯和不纯,并保证纯函数无法直接或间接地执行不纯函数。

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

但是,语言如何防止纯函数执行不纯函数?这是由于Haskell的惰性特性。仅当其输出被其他函数消耗时,才执行函数。但是没有办法消耗IO值,除非将其分配给main。因此,如果函数想要执行不纯函数,它必须连接到main并具有IO类型。

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

这将我们带到大多数人在Haskell中会写的第一个程序:

main :: IO ()

main = do

putStrLn "Hello World"

当只有单个操作且没有绑定时,do关键字是多余的,但为了一致性,我仍然保留它。

()类型表示“void”。此特殊返回类型仅对IO函数在意义上有用。

一个更长的示例:

main = do

putStrLn "What is your name?"

name <- getLine

putStrLn ("hello " ++ name)

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

将IO与Maybe进行比较可以显示单子模式的多功能性。对于Maybe,该模式用于通过将条件逻辑移到绑定函数中来避免重复代码。对于IO,该模式用于确保所有IO操作按顺序执行,并且IO操作不能“泄漏”到纯函数中。

总结

在我个人的观点中,单子模式只在语言对模式有一些内置支持的情况下才真正有价值。否则,它只会导致过于复杂的代码。但是Haskell(和其他一些语言)具有一些隐藏繁琐部分的内置支持,然后该模式可以用于各种有用的事情。例如:

避免重复代码(Maybe)

为程序的特定区域添加可变状态或异常

将令人讨厌的东西与好的东西隔离(IO)

嵌入领域特定语言(解析器)

为语言添加GOTO

我希望这篇文章对你有所帮助。但是,我仍然不知道单子是什么,更不用说简洁明了的解释了。

0