在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?

我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。


当前回答

Monad是一种带有特殊机器的盒子,它允许你从两个嵌套的盒子中制作一个普通的盒子,但仍然保持两个盒子的一些形状。

具体来说,它允许您执行连接,类型为Monad m=>m(m a)->m a。

它还需要一个返回操作,它只包装一个值。return::Monad m=>a->m a你也可以说joinunboxes和return wrappes,但join不是Monad m=>m a->a类型的(它不会打开所有Monad,而是打开Monad,Monad在其中)

所以它取一个Monad盒子(Monad m=>,m),里面有一个盒子((m a)),然后生成一个普通盒子(m a。

然而,Monad通常用于(>>=)(口语“bind”)运算符,它本质上只是一个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。现在,IO是对Haskell中的Action进行分类的类型。在这里,Monad系统是唯一的保存方式(华丽的词):

参考透明度懒惰纯洁

本质上,像getLine::IOString这样的IO操作不能被String替换,因为它总是具有不同的类型。把IO想象成一种神奇的盒子,可以把东西传送给你。然而,仍然只是说getLine::IOString和所有函数都接受IOa会导致混乱,因为可能不需要这些函数。const“üp§”getLine会做什么?(const丢弃第二个参数。const a b=a。)getLine不需要求值,但应该执行IO!这使得行为相当不可预测,也使得类型系统不那么“纯粹”,因为所有函数都将采用a和IOa值。

输入IO Monad。

要将动作串在一起,只需展平嵌套的动作。要将函数应用于IO操作的输出,IO a类型中的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

还有m>>=\_->f的>>运算符(当框中的值不需要在框中创建新框时)也可以写成a>>b=a>>=constb(consta b=a)

此外,返回运算符是根据IO直觉建模的-它返回一个具有最小上下文的值,在这种情况下没有IO。由于IO a中的a表示返回的类型,这类似于命令式编程语言中的return(a),但它不会停止操作链!f>>=return>>=g与f>>=g相同。仅当您返回的术语在链中较早创建时才有用-请参见上文。

当然,还有其他Monad,否则它不会被称为Monad,它会被称为“IO控制”之类的东西。

例如,List Monad(Monad[])通过串联变平-使(>>=)运算符对列表的所有元素执行函数。这可以被视为“不确定性”,其中列表是许多可能的值,而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 "= (:[])"

如果出现这种情况,“也许莫纳德”只会将所有结果作废为“无”。也就是说,绑定自动检查函数(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)

其他回答

在了解这些信息时,对我帮助最大的两件事是:

第8章,“函数解析器”,摘自Graham Hutton的《Haskell编程》一书。实际上,这根本没有提到monad,但如果您能够通读第章并真正理解其中的所有内容,特别是如何评估一系列绑定操作,您将了解monad的内部结构。预计这需要多次尝试。

关于修道院的教程。这提供了几个很好的例子来说明它们的用途,我不得不说,我在Appendex中的类比是为我工作的。

Monad用于控制流,就像抽象数据类型用于数据一样。

换句话说,许多开发人员对集合、列表、字典(或哈希、或地图)和树的概念很熟悉。在这些数据类型中有许多特殊情况(例如InsertionOrderPreservingIdentityHashMap)。

然而,当面对程序“流”时,许多开发人员还没有接触到比if、switch/case、do、while、goto(grr)和(可能)闭包更多的构造。

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

因此,monad具有用于控制逻辑、语句或函数的槽——数据结构中的等价物是,某些数据结构允许您添加数据,并删除数据。

例如,“if”monad:

if( clause ) then block

最简单的是有两个槽:一个子句和一个块。if monad通常用于评估子句的结果,如果不是false,则评估块。许多开发人员在学习“如果”时并没有接触到monad,而且编写有效的逻辑并不需要理解monad。

monad可能会变得更复杂,就像数据结构可能变得更复杂一样,但monad有很多大类可能具有相似的语义,但实现和语法不同。

当然,数据结构可以在单子上迭代或遍历,也可以以同样的方式进行评估。

编译器可能支持也可能不支持用户定义的monad。哈斯克尔当然知道。Ioke有一些类似的功能,尽管语言中没有使用monad一词。

经过努力,我想我终于明白了单子。在重新阅读了我自己对绝大多数投票结果的冗长批评之后,我将给出这个解释。

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

你为什么需要蒙纳德?什么是单子?如何实现monad?

正如我在最初的评论中所指出的,有太多的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%!");

这确实是一种更方便的写法。但我们如何做到这一点呢?

什么是单子?

一旦你定义了一个类型(比如consoleestate),以及一系列专门为该类型操作而设计的函数,你就可以通过定义一个操作符(比如:(bind))将这些东西的整个包变成一个“monad”,该操作符会自动将返回值输入到左边的函数参数中,转换为与特定类型的绑定运算符一起工作的函数。

如何实现monad?

看到其他答案,似乎可以很自由地跳到细节中。

http://mikehadlow.blogspot.com/2011/02/monads-in-c-8-video-of-my-ddd9-monad.html

这是你要找的视频。

用C#演示组合和对齐类型的问题,然后用C#正确实现它们。最后,他展示了F#和Haskell中相同的C#代码的外观。

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

“可能monad”本质上等同于Visual Basic/C#中的“可为null的类型”。它接受不可为null的类型“T”并将其转换为“可为null<T>”,然后定义所有二进制运算符在可为null><T>上的含义。

副作用也有类似的表现。创建了一个结构,该结构包含函数返回值旁边的副作用描述。当值在函数之间传递时,“提升”操作会复制副作用。

它们被称为“monad”,而不是更容易理解的“类型运算符”的名称,原因如下:

Monad对他们的行为有限制(详见定义)。这些限制,加上涉及三个运算,符合范畴理论中一个叫做monad的结构,这是一个模糊的数学分支。它们是由“纯”函数语言的支持者设计的纯函数语言的支持者,如模糊的数学分支由于数学晦涩难懂,而且monad与特定的编程风格相关,人们倾向于使用monad这个词作为一种秘密握手。正因为如此,没有人费心去投资一个更好的名字。