什么是monad?

最近简单地看了一下Haskell,对于monad实质上是什么而言,简单,简洁,实用的解释是什么?

我发现我所遇到的大部分解释都是相当难以获得的,并且缺乏实际的细节。


第一:如果你不是数学家, monad这个词有点空虚。 另一个术语是计算生成器 ,它对它们实际上有用的内容有更多描述。

你问一些实际的例子:

示例1:列表理解

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

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

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

do
   x <- [1..10]
   if odd x 
       then [x * 2] 
       else []

甚至:

[1..10] >>= (x -> if odd x then [x*2] else [])

示例2:输入/输出

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

这两个例子都使用monads,AKA计算构建器。 共同的主题是单子链操作以某种特定的,有用的方式进行。 在列表理解中,操作链接在一起,以便如果操作返回列表,则对列表中的每个项目执行以下操作。 另一方面,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。 这个例子是从唐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单元能够自行“分割”块并推迟后半部分的执行。 ( async {}语法指示块中的控制流由async单元定义。)

他们如何工作

那么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操作变成了一个非常好的和有用的抽象,在语言中很多地方很有用,并且对于在语言中创建您自己的迷你语言非常有用。

monad为什么很难?

对于许多Haskell学习者来说,monad是他们像砖墙一样撞到的障碍。 这并不是monads本身很复杂,而是实现依赖于许多其他高级Haskell功能,如参数化类型,类型类等等。 问题是Haskell I / O是基于monad的,而I / O可能是你想要学习一门新语言时要理解的第一件事情 - 毕竟,创建不产生任何程序的程序并不是很有趣输出。 对于这个鸡与鸡蛋问题,我没有立即的解决办法,除非把I / O当作“魔术发生在这里”,除非你对其他语言部分有足够的经验。 抱歉。

关于monads的优秀博客:http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html


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

什么是monad? 简短的回答:这是一个链接操作的具体方式。

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

所以绑定函数就像分号一样; 它将一个过程中的步骤分开。 绑定函数的工作是从上一步获取输出,并将其输入到下一步。

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

那么,绑定函数就可以从一个步骤中得到结果,并将其提供给下一步。 但是,如果这是“全部”,那么monad确实......并不是非常有用。 这是很重要的理解:除了只是一个monad之外,每个有用的monad都会做其他事情。 每个有用的monad都有一个“特殊的力量”,这使得它独一无二。

(一个没有任何特别之处的monad被称为“身份monad”,而不是身份函数,这听起来像是一个毫无意义的事情,但事实证明这不是......但这是另一个故事™。)

基本上,每个monad都有自己的绑定函数的实现。 而且你可以编写一个绑定函数,这样它就可以在执行步骤之间缠住一些东西。 例如:

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

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

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

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

  • 你可以这样做,以便可以替换“额外的数据”。 这使您可以模拟破坏性更新,而无需进行破坏性更新。 ( 国家Monad和它的表兄作家Monad 。)

  • 因为你只是在模拟破坏性更新,所以你可以轻而易举地做一些真正破坏性更新所不可能的事情。 例如,您可以撤消上一次更新,或者还原为较早版本。

  • 您可以创建一个monad,以便可以暂停计算,以便您可以暂停程序,进入并修改内部状态数据,然后重新开始。

  • 你可以实现“延续”作为monad。 这可以让你打破人们的思想!

  • 所有这些和更多是可能的单子。 当然,如果没有monad,所有这些也都是完全可能的。 使用monads会大大简化。


    实际上,与Monad的共同理解相反,它们与国家无关。 Monad只是一种包装物品的方式,并提供方法来对包装物品进行操作而不打开包装物品。

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

    data Wrapped a = Wrap a
    

    包装我们定义的东西

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

    为了在不展开的情况下执行操作,比如说你有一个函数f :: a -> b ,那么你可以这样做来解除这个函数对包装值的作用:

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

    这就是要了解的一切。 然而,事实证明,有一个更一般的功能来做这个提升,这是bind

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

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

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

    有关如何使用Monad来引入函数依赖关系并从而控制评估顺序(如Haskell的IO monad中使用它)的好文章,请查看IO Inside。

    至于理解单子,不要太担心。 阅读关于它们你感兴趣的内容,如果你不能立即理解,请不要担心。 然后,只需像Haskell这样的语言跳水就可以了。 Monad是通过实践理解流入大脑的这些东西之一,有一天你突然意识到你理解了它们。

    链接地址: http://www.djcxy.com/p/47661.html

    上一篇: What is a monad?

    下一篇: Monad in plain English? (For the OOP programmer with no FP background)