所有monad文章经常陈述,monad允许你按顺序排列效果。
但是简单的构图怎么样? Ain&#t; t
f x = x + 1
g x = x * 2
result = f g x
要求在g x
之前计算f ...
?
monad是否做同样的事情但处理效果?
答案 0 :(得分:13)
免责声明:Monads是很多东西。众所周知,它们难以解释,所以我不会试图解释一下一般的monad,因为这个问题没有要求。我将假设您已基本掌握Monad
接口的内容以及它对某些有用数据类型的工作原理,例如Maybe
,Either
和IO
。< / p>
您的问题以便笺开头:
所有monad文章经常陈述,monad允许你按顺序排列效果。
嗯。这是有趣的。实际上,有趣的原因有几个,其中一个原因已经确定:它暗示monads可以让你创建某种排序。这是真的,但它只是图片的一部分:还表示测序发生在效果。
这是事情,但是......什么是“效果”?将两个数字加在一起效果?在大多数定义下,答案是否定的。打印东西到stdout怎么样,这是一种效果吗?在这种情况下,我认为大多数人会同意答案是肯定的。但是,考虑一些更微妙的东西:通过产生Nothing
效果来短路计算?
我们来看一个例子。请考虑以下代码:
> do x <- Just 1
y <- Nothing
return (x + y)
Nothing
该示例的第二行由于Monad
的{{1}}实例而“短路”。这可能被认为是一种影响吗?从某种意义上说,我是这么认为的,因为它是非本地的,但在另一种意义上,可能不是。毕竟,如果交换Maybe
或x <- Just 1
行,结果仍然相同,所以排序无关紧要。
但是,请考虑使用y <- Nothing
代替Either
的稍微复杂的示例:
Maybe
现在这更有趣了。如果你现在交换前两行,你会得到不同的结果!不过,这是否就像你在问题中提到的“效果”一样?毕竟,它只是一堆函数调用。如您所知,> do x <- Left "x failed"
y <- Left "y failed"
return (x + y)
Left "x failed"
表示法只是do
运算符大量使用的替代语法,因此我们可以将其展开:
>>=
我们甚至可以用> Left "x failed" >>= \x ->
Left "y failed" >>= \y ->
return (x + y)
Left "x failed"
特定的定义替换>>=
运算符,以完全摆脱monad:
Either
因此,很明显monad确实会进行某种排序,但这不是因为它们是monad而monad是神奇的,只是因为它们碰巧启用了一种看起来的编程风格不像Haskell通常允许的那样。
但也许这对你不满意。错误处理并不引人注目,因为它只是短路,它实际上并没有对结果进行任何排序!好吧,如果我们找到一些稍微复杂的类型,我们就可以做到。例如,考虑> case Left "x failed" of
Right x -> case Left "y failed" of
Right y -> Right (x + y)
Left e -> Left e
Left e -> Left e
Left "x failed"
类型,它允许使用monadic接口进行某种“日志记录”:
Writer
这比以前更有趣,因为现在> execWriter $ do
tell "hello"
tell " "
tell "world"
"hello world"
块中每次计算的结果都未使用,但它仍会影响输出!这显然是副作用,顺序显然非常重要!如果我们重新排序do
表达式,我们会得到一个非常不同的结果:
tell
但这怎么可能?好吧,我们可以重写它以避免> execWriter $ do
tell " "
tell "world"
tell "hello"
" worldhello"
表示法:
do
我们可以再次为execWriter (
tell "hello" >>= \_ ->
tell " " >>= \_ ->
tell "world")
内联>>=
的定义,但这里的描述太长了。但问题是,Writer
只是一个完全普通的Haskell数据类型,不执行任何I / O或类似的操作,但我们使用monadic接口创建看起来像有序效果的东西。
我们可以通过使用Writer
类型创建一个看起来像可变状态的界面来进一步发展:
State
再一次,如果我们重新排序表达式,我们会得到不同的结果:
> flip execState 0 $ do
modify (+ 3)
modify (* 2)
6
显然,monad是一个非常有用的工具,用于创建看起来有状态的接口,并且具有明确定义的顺序,尽管实际上只是普通的函数调用。
是什么赋予了monads这种力量?好吧,它们不是魔术 - 它们只是普通的纯Haskell代码。但请考虑> flip execState 0 $ do
modify (* 2)
modify (+ 3)
3
的类型签名:
>>=
注意第二个参数如何依赖于(>>=) :: Monad m => m a -> (a -> m b) -> m b
,获得a
的唯一方法是来自第一个参数?这意味着a
需要“运行”第一个参数以生成值,之后它可以应用第二个参数。这与评估顺序没有关系,因为它与实际编写将要进行类型检查的代码有关。
现在,Haskell确实是一种懒惰的语言。但是Haskell的懒惰并不重要,因为所有这些代码实际上都是纯粹的,甚至是使用>>=
的例子!它只是一种模式,它以纯粹的方式编码看起来有状态的计算,但是如果你自己实际实现了State
,你会发现它只是绕过{{的定义中的“当前状态”。 1}}功能。没有任何实际的突变。
就是这样。 Monads凭借它们的接口,对如何评估它们的参数施加了一个排序,并且State
的实例利用它来创建有状态的接口。但是,正如您所发现的那样, >>=
不需要进行评估排序;显然在Monad
中,将在乘法之前评估加法。
Monad
?? 好的,你有我。问题在于:(1 + 2) * 3
是神奇的。
Monad不是魔术,但IO
是。以上所有示例都是纯粹的功能,但显然读取文件或写入stdout并不纯粹。那么IO
如何工作呢?
嗯,IO
是由GHC运行时实现的,你不能自己编写。但是,为了使其与Haskell的其余部分很好地协同工作,需要有一个明确定义的评估顺序!否则,事情会以错误的顺序打印出来,各种其他地狱都会破裂。
嗯,事实证明,IO
的界面是确保评估顺序可预测的好方法,因为它已经适用于纯代码。因此IO
利用相同的接口来保证评估顺序相同,并且运行时实际上定义了评估的含义。
但是,不要被误导!你不需要monad用纯语言做I / O,你不需要Monad
来产生monadic效果。 Early versions of Haskell experimented with a non-monadic way to do I/O,这个答案的其他部分解释你怎么能有纯粹的monadic效果。请记住,monads不是特殊或神圣的,它们只是Haskell程序员因其各种属性而发现有用的模式。
答案 1 :(得分:6)
是的,您建议的功能对标准数字类型是严格的。但并非所有功能都是!在
f _ = 3
g x = x * 2
result = f (g x)
<{1}}之前必须计算g x
的情况。
答案 2 :(得分:2)
是的,monads使用函数组合来排序效果,并不是实现排序效果的唯一方法。
在大多数语言中,首先将严格语义应用于表达式的函数端,然后依次应用于每个参数,最后将该函数应用于参数。所以在JS中,函数应用程序表单
<Code 1>(<Code 2>, <Code 3>)
以指定的顺序运行四个代码:1,2,3,然后它检查1的输出是否为函数,然后用这两个计算的参数调用该函数。这样做是因为任何这些步骤都会产生副作用。你会写,
const logVal = (log, val) => {
console.log(log);
return val;
};
logVal(1, (a, b) => logVal(4, a+b))(
logVal(2, 2),
logVal(3, 3));
这适用于那些语言。这些是副作用,我们可以说在这个上下文中意味着JS的类型系统不会让你知道它们在那里。
Haskell确实有一个严格的应用程序原语,但它想要纯,这大致意味着它希望类型系统跟踪效果。因此,他们引入了一种元编程形式,其中一种类型是类型级形容词,“计算_____的程序”。一个程序与现实世界互动;理论上Haskell代码没有。您必须定义“main
是一个计算单元类型的程序”,然后编译器实际上只是为您构建该程序作为可执行的二进制文件。当文件运行时 Haskell不再是真正的图片了!
因此,比普通函数应用程序更具体,因为我用JavaScript编写的抽象问题是,
那不是只是功能组合本身。但功能可以那个。
monad是一种模式。模式是,有时你有一个形容词,当你重复它时,它不会增加太多。例如,当您说“a 延迟延迟x”或“零或更多(零或更多xs)”或“无论是null还是null或x时,添加的内容都不多。 “类似地,对于IO monad,“用于计算计算x的程序的程序”并没有增加多少“在计算x的程序中不可用”。
模式是有一些合并的规范合并算法:
加入:给定
<adjective> <adjective> x
,我会为您<adjective> x
。
我们还添加了两个其他属性,形容词应该是 outputtish ,
地图:给定
x -> y
和<adjective> x
,我会为您<adjective> y
和普遍可嵌入,
纯:给定
x
,我会让你成为<adjective> x
。
鉴于这三件事和一些公理,你碰巧有一个共同的“monad”想法,你可以为它开发一个真正的语法。
现在这个元编程的想法显然包含了一个monad。在JS中我们会写,
interface IO<x> {
run: () => Promise<x>
}
function join<x>(pprog: IO<IO<x>>): IO<x> {
return { run: () => pprog.run().then(prog => prog.run()) };
}
function map<x, y>(prog: IO<x>, fn: (in: x) => y): IO<y> {
return { run: () => prog.run().then(x => fn(x)) }
}
function pure<x>(input: x): IO<x> {
return { run: () => Promise.resolve(input) }
}
// with those you can also define,
function bind<x, y>(prog: IO<x>, fn: (in: x) => IO<y>): IO<y> {
return join(map(prog, fn));
}
但是存在模式的事实并不意味着它有用!我声称这些功能结果是你需要的所有来解决上面的问题。并且不难看出原因:您可以使用bind
来创建一个不存在形容词的函数范围,并在那里操纵您的值:
function ourGoal<x, y, z>(
fnProg: IO<(inX: x, inY: y) => IO<z>>,
xProg: IO<x>,
yProg: IO<y>): IO<z> {
return bind(fnProg, fn =>
bind(xProg, x =>
bind(yProg, y => fn(x, y))));
}
请注意,在上面我们通过编写三个bind
的方式选择操作顺序。我们本可以用其他顺序写出来。但我们需要所有参数来运行最终程序。
我们如何对操作进行排序的选择确实是在函数调用中实现:你是100%正确。但是你这样做的方式,用< em> only 功能组合,是有缺陷的,因为它会将效果降级为副作用,以便通过它来获取类型。