在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
在最近简要回顾了Haskell之后,对于monad本质上是什么,有什么简单、简洁、实用的解释?
我发现,我遇到的大多数解释都很难理解,而且缺乏实际细节。
当前回答
遵循您简短、简洁、实用的指示:
理解monad最简单的方法是在上下文中应用/组合函数。假设你有两个计算,它们都可以看作是两个数学函数f和g。
f取一个String并生成另一个String(取前两个字母)g获取一个String并生成另一个String(大写转换)
因此,在任何语言中,“取前两个字母并将其转换为大写”的转换都会写成g(f(“某个字符串”))。因此,在纯完美函数的世界中,合成只是:先做一件事,然后再做另一件事。
但假设我们生活在一个功能可能失败的世界中。例如:输入字符串可能有一个字符长,因此f将失败。所以在这种情况下
f获取一个String并生成一个String或Nothing。g仅在f未失败时生成字符串。否则,将不生成任何内容
所以现在,g(f(“somestring”))需要一些额外的检查:“计算f,如果它失败,那么g应该返回Nothing,否则计算g”
此思想可应用于任何参数化类型,如下所示:
让Context[Sometype]是Context中Sometype的计算。考虑功能
f: :AnyType->上下文[Sometype]g: :某些类型->上下文[AnyOtherType]
合成g(f())应该读作“compute f。在这个上下文中,做一些额外的计算,然后计算g,如果它在上下文中有意义”
其他回答
(另请参见“什么是monad?”中的答案)
蒙纳斯的一个很好的动机是西格菲(丹·皮波尼)的《你本可以发明蒙纳斯!(也许你已经有了)。还有很多其他monad教程,其中许多都试图使用各种类比以“简单的术语”来解释monad:这就是monad教程谬论;避开它们。
正如MacIver博士在《告诉我们为什么你的语言很糟糕》中所说:所以,我讨厌Haskell的事情:让我们从显而易见的开始。Monad教程。不,不是单子。特别是教程。他们没完没了,夸夸其谈,亲爱的上帝,他们太乏味了。此外,我从未见过任何令人信服的证据表明它们确实有帮助。阅读类定义,编写一些代码,忘掉这个可怕的名字。
你说你懂“也许莫纳德”吗?很好,你在路上了。只要开始使用其他monad,迟早你会了解monad的一般含义。
(如果你以数学为导向,你可能想忽略几十个教程,学习定义,或遵循类别理论的讲座:)定义的主要部分是Monad M包含一个“类型构造器”,为每个现有类型“T”定义一个新类型“M T”,以及在“常规”类型和“M”类型之间来回移动的一些方式。]
同样,令人惊讶的是,对monad最好的介绍之一实际上是介绍monad的早期学术论文之一,Philip Wadler的Monad for functional programming。它实际上有一些实用的、非平凡的激励性例子,与许多人工教程不同。
monad实际上是“类型运算符”的一种形式。它将做三件事。首先,它会将一种类型的值“包装”(或以其他方式转换)为另一种类型(通常称为“一元类型”)。第二,它将使底层类型上的所有操作(或函数)在monadic类型上可用。最后,它将为将自身与另一个monad组合以生成复合monad提供支持。
“可能monad”本质上等同于Visual Basic/C#中的“可为null的类型”。它接受不可为null的类型“T”并将其转换为“可为null<T>”,然后定义所有二进制运算符在可为null><T>上的含义。
副作用也有类似的表现。创建了一个结构,该结构包含函数返回值旁边的副作用描述。当值在函数之间传递时,“提升”操作会复制副作用。
它们被称为“monad”,而不是更容易理解的“类型运算符”的名称,原因如下:
Monad对他们的行为有限制(详见定义)。这些限制,加上涉及三个运算,符合范畴理论中一个叫做monad的结构,这是一个模糊的数学分支。它们是由“纯”函数语言的支持者设计的纯函数语言的支持者,如模糊的数学分支由于数学晦涩难懂,而且monad与特定的编程风格相关,人们倾向于使用monad这个词作为一种秘密握手。正因为如此,没有人费心去投资一个更好的名字。
我也在努力理解单子。这是我的版本:
Monad是关于对重复的事物进行抽象的。首先,monad本身是一个类型化接口(像抽象泛型类),它有两个函数:bind和return,它们定义了签名。然后,我们可以基于抽象的monad创建具体的monad,当然还有绑定和返回的具体实现。此外,绑定和返回必须满足几个不变量,以便可以组合/链接具体的单体。
当我们有接口、类型、类和其他工具来创建抽象时,为什么要创建monad概念?因为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)
解释monad似乎就像解释控制流语句一样。想象一下,一个非程序员要求你解释它们?
你可以给他们一个涉及理论的解释——布尔逻辑、寄存器值、指针、堆栈和框架。但那太疯狂了。
你可以用语法来解释它们。基本上,C中的所有控制流语句都有大括号,您可以通过它们相对于括号的位置来区分条件和条件代码。这可能更疯狂。
或者,您也可以解释循环、if语句、例程、子例程以及可能的协例程。
Monad可以取代相当多的编程技术。语言中有一种特定的语法支持它们,还有一些关于它们的理论。
它们也是函数式程序员使用命令式代码而不承认它的一种方式,但这并不是他们唯一的用途。