Monads(八)

上次我们找到了monad pattern的最终规则;当然,在之前我们就知道了这个模式的重点部分是那个叫做CreateSimpleM<T>帮助构造对象的函数,以及叫做ApplySpecialFunction<A, R>的函数。不用说,这些名称肯定不是他们的正式命名(不过如果这些真的是正式命名的话,恐怕学起来会快得多,希望我的这些小改动有帮到你)。

对于CreateSimpleM<T>,他的传统名称是unit,我觉得这是有一定意义的。在大量应用monads的纯函数式语言Haskell中,unit函数叫做return,如果你没有Haskell基础的话,这一点解释起来会比较麻烦(Haskell的初学者经常被return实际上是函数而非关键字这一点混淆)。在C#中,这些东西并没有一个标准的规范,对于我们前文中提到的五种Monadic类型来说,有五种不同的方法来用一个简单的值去创建一个他们的实例:

Nullable<int> nullable = new Nullable<int>(123);
Task<int> task = Task.FromResult<int>(123);
Lazy<int> lazy = new Lazy<int>(() => 123);
OnDemand<int> onDemand = () => 123;
IEnumerable<int> sequence = Enumerable.Repeat<int>(123, 1);

说实话,最有一个有点问题,我其实希望Enumerable有一个专门用来创建单元素序列的静态方法。

而对于ApplySpecialFunction<A, R>,他的传统名称叫做bind,在Haskell中bind函数实际上是一个中缀运算符(中缀运算符指位置在两个运算数之间的运算符,比如加减乘除都是中缀的);在Haskell中,如果你要把一个函数f应用到一个monad实例m上,你需要写作m >>= f。在C#中bind一般不会显式提供,因此也没有一个特别的名字(一个极为重要的例外是作用在序列上的bind函数是Enumerable.SelectMany,我会在以后的章节中讲它)。

"Unit"也许还有点意义,但是bind究竟是什么意思?Haskell里面那个古怪的语法又是什么意思呢?

你也许注意到了,Task, Lazy, OnDemand, IEnumerable都有一个有趣的共同特征: 当你对这些类型应用一个函数的时候,你得到的实际上是一个会在未来执行对应函数内容的对象,简而言之,bind函数接受一个不可变的流和要对其进行的操作,然后返回一个新的流,所以m >>= f的意思就是"把操作f绑定(bind)到流m的尾部,并把作为结果的新流交给我"。在这里,Haskell的语法实际上非常合适,就像是流m把自己结果"输入"进函数f一般。

让我们来总结一下: bind操作接受一个流和一个函数并且返回一个新的流,这个新的流在被执行(execute,也可以说激活)时会把旧的流的结果输入进函数中,注意,bind操作本身并不会执行这个流,他只是在旧流的基础上创建了一个新的流(就像C#的linq,在旧的查询操作基础上创建新的查询操作并不会去执行这个新的操作。这并非偶然,LINQ的设计者同样是一位Haskell的设计者,Eirk Meijier。LINQ查询语法实际上是monad绑定语法的一种变式,我们会在以后的章节中讨论)

至于这些流究竟被如何执行,则取决于对应的monad的语义,比如IEnumerableMoveNext被调用时执行,直到流中的下一个元素可以被计算出来为止。而Lazy则在你第一次访问Value属性的时候执行,在那之后它将会使用缓存的值。而OnDemand会在委托被调用的时候执行。Task则在自身被调度到的时候执行。

这就是这些特别的monad的意义所在:他们代表了一系列任务和执行这些任务的顺序。相比之下,Nullable则要简单得多(顺便,它在其他语言里一般叫做Maybe monad,意为这里可能有值,也可能没有),它并不包含一个可能在未来执行的流,而只是代表了一个附加在值上的额外状态——也即一个Boolean,对Nullable进行的计算看上去都是非常"急切"的立即完成了,而非拖延到以后完成。


行成于思,毁于随