Monad(七)

时间回到1992年,我(是原作者不是我!)正在滑铁卢大学学习线性代数,当时脑子里面每天都充斥着对偶空间的概念,百思不得其解。有天晚上睡觉之后甚至梦到了它,神奇的是,在我睡醒之后,一切的疑惑突然就在脑海中迎刃而解了。一直到现在,我依然认为这是大脑在睡觉的时候帮我整理了知识点,如此奇妙的体验我尚未经历第二次(很遗憾的时候从那以后在对偶空间这个知识点上再也没有那天那种恍然大悟的感觉了,这20年以来我都没有用过比基本的线性代数知识更高端的玩意,如果有需要的话我也许会重拾起它,但是我相信那种感觉再也不会回来了)。实际上历史上类似这种恍然大悟最终解决悬案的例子比比皆是。英年早逝的天才数学家拉马努金曾说过他曾在梦中畅游古老而又恢弘的数学史诗,而在这其中迸发而出的灵感,大部分都毋庸置疑并且精妙绝伦。

当然,守株待兔式的等着答案在梦中找上门是愚蠢而又困难的事情:你永远也不知道什么时候才会发生。既然这种类似神谕一般的方法是靠不住的,我们就创造了一种相比之下可靠得多的方法来解决问题:递归/分治。我们使用和递归函数一样的思路来解决问题:

当前的问题是否足够简单?如果是就直接计算出答案,否则,把当前的问题分解为数个相同但是规模更小的子问题,对这些子问题再施行相同的操作,最终把所有子问题的结果合并起来,成为原先问题的结果

今天我想谈的有关这其中的"组合"部分,所谓组合,指的就是把两个及以上的小规模问题的答案,组合成原先规模问题的答案,这种思想在日常程序设计中如同空气一般常见——也如同空气一般容易被忽视。下面我们就给出了两个属性的组合范式,结果是第三个属性:

public double Area
{
    get { return this.Length * this.Width; }
}

再用矩形举例子,无论是什么样的矩形,都可能会有一个由数个Point类型构成的组合来表示它的角,而Point又会采取两个double类型的组合来表示一个坐标。现代编程语言的所有机制,都是为了保证能够更简单的把小规模问题的答案合并为更大规模问题的答案,因此对于C#程序员来说,有千百种组合的方式。今天我想要讨论的是一种非常特殊的组:一个一元非空(void)的函数的组合(其实有点争议,有些学者认为两个没有参数的空(void)函数更加基础一些),举一个很浅显的例子,如果你有如下两个函数:

static long Cube(int x) { return (long) x * x * x; }
static double Halve(long y) { return y / 2.0; }

你就可以把它们组合成第三个函数:

static double HalveTheCube(int x) { return Halve(Cube(x)); }

事实上在编写程序的时候,程序文本本身就描述了一大堆的"组合",而每一个都要比这种一元函数的组合更加复杂。我们也可以动态的去执行这些组合操作,比如使用委托(delegates):

Func<int, long> cube = x => (long) x * x * x;
Func<long, double> halve = y => y / 2.0;
Func<int, double> both = z => halve(cube(z));

甚至可以更进一步的写一个泛化的函数:

static Func<X, Z> Compose<X, Y, Z>(Func<X, Y> f, Func<Y, Z> g)
{
    return x => g(f(x));
}

然后我们就可以稍微改写一下上面的例子:

Func<int, long> cube = x => (long) x * x * x;
Func<long, double> halve = y => y / 2.0;
Func<int, double> both = Compose(cube, halve);

当然你肯定不会真的去这么做,因为C#中对于方法组合已经有足够简单的语法支持了。但是上面这段程序很形象的表达了你日常编程中把一个函数的结果传给另一个函数的参数实际上就是把他们组合成为一个新的函数。同时也要注意到,为了保证这个组合是有效的,组合"里面"的那个函数的返回值必须可以被隐式的转换为"外部"那个函数的参数类型。而这也会为我们介绍Monad Pattern的最后一个有关类型的规范。我们已经在很长一段时间里讨论了有关一些返回Monadic类型的特殊函数,类似的,假设我们有如下两个函数:

Func<int, Nullable<double>> log = x => x > 0
    ? new Nullable<double>(Math.Log(x))
    : new Nullable<double>();
Func<double, Nullable<decimal>> toDecimal = y => Math.Abs(y) < decimal.MaxValue
    ? new Nullable<decimal>((decimal)y)
    : new Nullable<decimal>();
Func<int, Nullable<decimal>> both = Compose(log, toDecimal);

这个组合当然是没用的,因为toDecimal接受一个double,而log返回一个Nullable<double>,很明显我们想要的操作是"如果log的结果为null,那么组合后的函数结果也为null,反之就应当把log返回的Nullable<double>所包装的double传递给toDecimal",看到这里回想一下,ApplySpecialFunction不是正好解决了这个问题吗:

static Func<X, Nullable<Z>> ComposeSpecial<X, Y, Z>(Func<X, Nullable<Y>> f, Func<Y, Nullable<Z>> g)
{
    return x => ApplySpecialFunction(f(x), g);
}

通过这个示例可以发现振奋人心的一点:ApplySpecialFunction不仅可以把任何一个函数应用在一个Monad类型上,也可以把任意两个返回相同Monad类型的函数组合起来!而上次说到在这一节会引出的"最后一条规范",就是指"ApplySpecialFunction必须保证这种组合是有效的"。举个例子,在如下代码中:

Func<X, M<Y>> f = whatever;
Func<Y, M<Z>> g = whatever;
M<X> mx = whatever;
M<Y> my = ApplySpecialFunction(mx, f);
M<Z> mz1 = ApplySpecialFunction(my, g);
Func<X, M<Z>> h = ComposeSpecial(f, g);
M<Z> mz2 = ApplySpecialFunction(mx, h);

这里要求mz1mz2应当是语义一致的,对一个值先用f函数再用g函数,应当和对这个值使用fg的组合有相同的效果(再再再次提醒,这里不需要引用一致,你能做到那自然非常pro,但是这里真的不需要,语义一致就够了),接下来,来总结出Monad Pattern的最终法则吧:

一个Monad是一个泛型类型Monadic<T>满足:
1. 存在一种简单的构造机制能将任意的T转换为Monadic<T>static M<T> CreateSimpleM<T>(T t)
2. 我们可以将一个接受T的函数应用给Monadic<T>static M<R> ApplySpecialFunction<A, R>(M<A> monad, Func<A, M<R>> function)
而这两个函数都必须遵循以下的Monad法则:
1. 将构造函数(这里指的是构造Monadic<T>的简单函数)应用在一个Monadic<T>上应当返回一个语义一致的Monadic<T>
2. 将一个函数应用在构造函数产生的值与直接应用在那个值本身上应当产生语义一致的Monadic类型的值
3. 对一个值依次应用两个函数,应当和对这个值应用两个函数的组合函数产生两个语义一致的Monadic类型的值

看完这也许依然令你有点困惑的一切,你大概就明白为什么我没有在一开始就上概念讲理论了,在下一篇中,我会解释这些机制一般都被称作什么


明志 责己 笃行 求真