协慌网

登录 贡献 社区

什么是 monad?

最近简要介绍了 Haskell,对于 monad 本质上是什么,简单,简洁,实用的解释是什么?

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

答案

第一:如果你不是数学家, monad这个词有点空洞。另一个术语是计算构建器 ,它更具描述性,它们实际上是有用的。

你问一些实际的例子:

示例 1:列表理解

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

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

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

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

甚至:

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

示例 2:输入 / 输出

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

两个示例都使用 monad,AKA 计算构建器。共同的主题是 monad 以一些特定的,有用的方式运作。在列表推导中,操作被链接,使得如果操作返回列表,则对列表中的每个项执行以下操作。另一方面,IO monad 按顺序执行操作,但传递 “隐藏变量”,表示 “世界状态”,这允许我们以纯函数方式编写 I / O 代码。

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

另一个例子是异常:使用Error monad,操作被链接,使得它们按顺序执行,除非抛出错误,在这种情况下,链的其余部分被放弃。

list-comprehension 语法和 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))

chardigit等操作非常简单。它们匹配或不匹配。神奇的是管理控制流的 monad:操作按顺序执行,直到匹配失败,在这种情况下,monad 回溯到最新的<|>并尝试下一个选项。同样,一种使用一些额外的,有用的语义来链接操作的方法。

例 4:异步编程

上面的例子在 Haskell 中,但事实证明F#也支持 monad。这个例子是从Don Syme偷来的:

let AsyncHttp(url:string) =
    async {  let req = WebRequest.Create(url)
             let! rsp = req.GetResponseAsync()
             use stream = rsp.GetResponseStream()
             use reader = new System.IO.StreamReader(stream)
             return reader.ReadToEnd() }

此方法获取网页。妙语是使用GetResponseAsync - 它实际上等待单独线程上的响应,而主线程从函数返回。当收到响应时,最后三行在生成的线程上执行。

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

他们如何工作

那么 monad 怎么能做所有这些奇特的控制流程呢?在 do-block(或在 F#中调用的计算表达式)中实际发生的是每个操作(基本上每一行)都包含在一个单独的匿名函数中。然后使用bind运算符(在 Haskell 中拼写>>= )组合这些函数。由于bind操作组合了函数,它可以按照它认为合适的方式执行它们:顺序,多次,反向,丢弃一些,在感觉它时在一个单独的线程上执行一些,依此类推。

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

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

这是更加丑陋的,但实际发生的事情也更加明显。 >>=运算符是神奇的成分:它取一个值(在左侧)并将它与一个函数(在右侧)组合,以产生一个新值。然后,这个新值由下一个>>=运算符获取,并再次与函数组合以产生新值。 >>=可以被视为迷你评估者。

请注意, >>=因不同类型而重载,因此每个 monad 都有自己的>>=实现。 (链中的所有操作都必须是同一个 monad 的类型,否则>>=运算符将不起作用。)

最简单的可能实现>>=只取左边的值并将其应用于右边的函数并返回结果,但如前所述,使整个模式有用的原因是当有一些额外的事情发生时 monad 的实现>>=

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

加起来

在 Haskell 术语中,monad 是一个参数化类型,它是 Monad 类型的一个实例,它定义了>>=以及一些其他运算符。用外行人的话来说,monad 只是一种定义了>>=操作的类型。

本身>>=只是链接函数的一种繁琐的方式,但是由于存在隐藏 “管道” 的记号,monadic 操作变成了一个非常好的和有用的抽象,在语言的许多地方很有用,对于使用该语言创建自己的迷你语言非常有用。

为什么单子难?

对于许多 Haskell 学习者来说,monad 是他们像砖墙一样的障碍。并不是 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

解释 “什么是 monad” 有点像说 “什么是数字?” 我们一直使用数字。但想象你遇到了一个对数字一无所知的人。你会如何赫克解释的数字是什么?你怎么会开始描述为什么这可能有用呢?

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

实质上,您正在编写执行步骤并使用 “绑定功能” 将它们链接在一起。 (在 Haskell 中,它被命名为>>= 。)您可以自己编写对 bind 操作符的调用,或者您可以使用语法 sugar,使编译器为您插入这些函数调用。但无论哪种方式,每个步骤都通过调用此绑定函数来分隔。

所以 bind 函数就像一个分号; 它分离了一个过程中的步骤。绑定功能的作用是获取上一步的输出,并将其输入下一步。

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

好吧,绑定功能可以从一步获取结果,并将其提供给下一步。但是,如果这是 “所有”monad 所做的...... 那实际上并不是非常有用。这一点很重要:每个有用的 monad 除了成为 monad 之外还会做其他事情。每一个有用的单子都有 “特殊的力量”,这使它独一无二。

(没有什么特别之处的 monad 被称为 “身份 monad”。与身份功能相似,这听起来像一个完全没有意义的事情,但结果却不是...... 但那是另一个故事™。)

基本上,每个 monad 都有自己的 bind 函数实现。您可以编写一个绑定函数,以便在执行步骤之间进行连接。例如:

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

  • 扩展这个想法,你可以实现 “例外”。 ( 错误 MonadException Monad 。)因为您自己定义它们而不是语言功能,所以您可以定义它们的工作方式。 (例如,您可能希望忽略前两个异常,并且仅在抛出第三个异常时中止。)

  • 您可以使每个步骤返回多个结果 ,并使绑定函数循环遍历它们,将每个步骤提供给下一步。这样,在处理多个结果时,您不必在整个地方继续编写循环。绑定功能 “自动” 为您完成所有这些。 ( 名单 Monad 。)

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

  • 您可以将其设置为可以替换 “额外数据”。这允许您模拟破坏性更新 ,而无需实际进行破坏性更新。 ( 国家莫纳德及其堂兄作家莫纳德 。)

  • 因为您只是在模拟破坏性更新,所以您可以通过真正的破坏性更新轻松完成这些操作。例如,您可以撤消上次更新 ,或恢复为旧版本

  • 您可以创建一个可以暂停计算的 monad,这样您就可以暂停程序,进入并修改内部状态数据,然后恢复它。

  • 您可以将 “continuation” 实现为 monad。这可以让你打破人们的思想!

monad 可以实现所有这些以及更多。当然, 如果没有 monad,所有这一切也是完全可能的。使用 monads 非常容易

实际上,与莫纳德的共同理解相反,他们与国家无关。 Monads 只是一种包装东西的方法,并提供了对包裹的东西进行操作的方法,而无需展开它。

例如,您可以在 Haskell 中创建一个类型来包装另一个类型:

data Wrapped a = Wrap a

包装我们定义的东西

return :: a -> Wrapped a
return x = Wrap x

要在不解包的情况下执行操作,假设您有一个函数f :: a -> b ,那么您可以执行此操作来解除该函数以对包装值执行操作:

fmap :: (a -> b) -> (Wrapped a -> Wrapped b)
fmap f (Wrap x) = Wrap (f x)

这就是所有需要了解的内容。然而,事实证明,有一个更通用的功能来完成这个提升 ,这是bind

bind :: (a -> Wrapped b) -> (Wrapped a -> Wrapped b)
bind f (Wrap x) = f x

bind可以比fmap做更多,但反之亦然。实际上, fmap只能在bindreturn方面定义。所以,在定义一个 monad 时... 你给它的类型(这里是Wrapped a ),然后说出它的returnbind操作是如何工作的。

很酷的是,事实证明这是一种普遍的模式,它会在整个地方弹出,以纯粹的方式封装状态只是其中之一。

有关如何使用 monad 来引入函数依赖关系并因此控制评估顺序的好文章,就像它在 Haskell 的 IO monad 中使用一样,请查看IO Inside

至于理解 monad,不要太担心它。阅读他们您感兴趣的内容,如果您不理解,请不要担心。然后,只需要像 Haskell 这样的语言潜水就可以了。 Monads 是通过练习理解涓涓细流到你的大脑的其中一件事,有一天你突然意识到你理解它们。