Monads(六)

在上一篇中我们终于探索出了Monad Pattern真正的规范,也就是在C#中,一个Monad应该是一个"扩充了"其泛型参数功能的泛型类型Monadic<T>,对于所有的Monadic<T>,都存在一个简单的方法,可以将T转换为Monadic<T>,我们将其表达为如下的函数:

static Monadic<T> CreateSimpleM<T>(T t)

并且如果有一个形如f: A->Monadic<R>的函数,则同样会有一个形如f: Monadic<T>->Monadic<R>的函数。我们用如下的C#函数来表示它:

static Monadic<R> ApplySpecialFunction(Monadic<A> wrapped, Func<A, Monadic<R>> function)

然而这两点并非Monad Pattern的全部规范。这两个函数需要一些额外的限制才能够保证正常运行。特别的,我们可以把第一个函数当做包装一个值的函数,而第二个函数知道如何"取出"这个值,这么看来,要求这两个操作保留其内部的值是非常合理的。有了这一点之后,可以注意到ApplySpecialFunction的第二个参数是一个函数类型f: A->Monadic<R>,并且AR都是未限定的,可以是任意的A,也可以是任意的R,但是CreateSimpleM却是从TMonadic<T>,因此可以进一步得出一个结论:CreateSimpleM也许可以作为ApplySpecialFunction的参数,例如之前提到过的如下代码:(从现在开始我不会再用冗长的方式去写这些函数了)

static Nullable<T> CreateSimpleNullable<T>(T t)
{
    return new Nullable<T>(t);
}

static Nullable<R> ApplySpecialFunction<A, R>(Nullable<A> nullable, Function<A, Nullable<R>> function)
{
    return nullable.HasValue ? function(nullable.Value) : new Nullable<R>();
}

注意到CreateSimpleNullable的函数签名与ApplySpecialFunction的第二个参数是相吻合的,因此可以这么写:

Nullable<int> original = Whatever();
Nullable<int> result = ApplySpecialFunction(original, CreateSimpleNullable);

让我们来一步一步了解这里发生了什么:如果originalnull,那么ApplySpecialFunction就简单的返回一个新的Nullable<int>,而如果original有一个值,比方说是12,那么就会将这个值从original里"取"出来,传给CreateSimpleNullable,然后就得到了一个新的被包装进Nullable<int>12,这里引申出一条新的规范:

对一个monad值执行一个"简单的包装操作"应当产生一个和原来一模一样的monad值

在上面的例子里我们保证了值的一致性。现在,应当注意到我们并不需要"引用一致性"。考虑之前提到过的一种monad类型OnDemand<T>

static OnDemand<T> CreateSimpleOnDemand<T>(T t)
{
    return () => t;
}

static OnDemand<R> ApplySpecialFunction<A, R>(OnDemand<T> onDemand, Func<A, OnDemand<R>> function)
{
    return () => function(onDemand())();
}

假设有:

OnDemand<int> original = () => DateTime.Now.Seconds;
OnDemand<int> result = ApplySpecialFunction(original, CreateSimpleOnDemand);

很明显originalresult在引用上是不一致的,但是他们的语义相同:调用时会返回的当前的秒数。后者也许要多那么几个步骤来执行,但是总而言之,这两者之间并没有什么不同,一些Monad Pattern的实现中可以轻松的以非常低的代价做到引用一致性,虽然说这是一件很棒的事情,但是实际上我们只需要原来的值和经过SimpleCreateM变换之后的值保证语义一致就可以了。

下一个限制是如何精确的表达"对一个值的包装",我们可以通过上方及其之前提到的CreateSimpleMApplySpecialFunction轻松做到这点,回想一下我们之前的SafeLog函数:

static Nullable<double> SafeLog(int value) { ... }

int original = 123;
Nullable<double> result1 = SafeLog(original);
Nullable<int> nullable = CreateSimpleNullable(original);
Nullable<double> result2 = ApplySpecialFunction(nullable, SafeLog);

你可能觉得result1result2应该是同一个值,毕竟,如果Nullable<int>只是一个简单的对于int的包装,那么对他应用一个函数应该就和对内部的int应用有相同的效果。可以把这个规范推广到全体Monad:假设我们有一个T类型的值,那么对其应用一个函数得到的结果,应当与将这个函数应用给包装了该T的Monad的结果相同(再次提醒:不需要引用一致,只需要语义一致)

到此为止,让我们再来做一次总结吧。所谓Monad Pattern的规范,就是指一个Monad类型Monadic<T>需要提供与下列两个函数作用等同的方法:

static Monadic<T> CreateSimpleM<T>(T t) { ... }
static Monadic<R> ApplySpecialFunction<A, R>(Monadic<A> monad, Func<A, Monadic<R>> function) {...}

并且需要保证:

ApplySpecialFunction(someMonadValue, CreateSimpleM)

会产生一个和someMonadValue语义一致的值,并且

ApplySpecialFunction(CreateSimpleM(someValue), someFunction)

会产生一个和someFunction(someValue)语义一致的值

小结

我们tm的终于讲完Monad Pattern的规范了吗?

遗憾的是,没有,还是有一条规范没有在本文中被提及,不过我保证这是最后一次了,在下一篇文章中,我们会讨论编程的本质,如何用编程解决问题,以及Monad Pattern是如何在这方面大放异彩的。当然,那条最后的规范也会在下一篇中被推导出来


乱我心者,今日之日多烦忧