C# Monads的实现(二)
再谈continuation monad
上一篇中我们已经介绍了continuation monad,但是这个monad与Identity,Maybe,IEnumerable monads稍微难于理解,故本篇再次讨论。
首先解决上一篇中最后关于continuation monad的问题,即以下这段代码目前还无法通过编译,
var r = from x in 7.ToContinuation<int, string>() from y in 6.ToContinuation<int, string>() select x + y; Console.WriteLine(r(z => z.ToString().Replace('1', 'a'))); // displays a3
比较前面Identity monad中SelectMany实现了两个版本,可以发现对于continuation monad,我们需要实现其SelectMany的另一个版本,即除了this指定的参数,还有两个输入参数。
对比Identity monad中相应的SelectMany实现,容易知道,新增的参数是一个Func 函数,在我们上面这个例子中,这个Func 函数的lambda表达式体就是 x + y,故不难写出SelectMany的函数签名,如下,用(1)标注
public static K<V, Answer> SelectMany<T, U, V, Answer>(this K<T, Answer> m, Func<T, K<U, Answer>> k, Func<T, U, V> s); (1)
这里我们也新增了一个泛型参数V,虽然对我们这个例子来说,T,U,V都是int,Answer是string,但是我们考虑了更一般的情况,故用三个泛型参数T,U,V来表示。
为了方便,我们首先把之前的SelectMany版本再次写出来,免得到上一篇文章中找寻,
public static K<U, Answer> SelectMany<T, U, Answer>(this K<T, Answer> m, Func<T, K<U, Answer>> k) { return (Func<U, Answer> c) => m((T x) => k(x)(c)); }
那么,如何实现(1)的SelectMany函数?
分析:
返回类型为K<V, Answer>,故函数体可以写成类似如下,
return m.SelectMany<T, V, Answer>(...);
这句代码中的SelectMany记为之前我们已经实现了的重载版本,根据这个重载版本,我们知道括号中...部分为一个输入参数,类型为Func<T, K<V, Answer>>,故此时(1)的函数体如下,
return m.SelectMany<T, V, Answer>((T x) => ...)
现在,这句代码中的...部分的返回类型为K<V, Answer>,此为最终的返回类型,那如何得到这样一个类型值?
首先,要得到V类型,只能通过s获得,而s的参数类型为T, U,T类型值已知,为x,还需要一个U类型值,U类型值只能通过k获得,应用k到x上得到K<U, Answer>,对这个K<U, Answer>应用已实现的SelectMany重载版本,即可去包装化K<U, Answer>,如下所示,
k(x).SelectMany(...)
为了与最终的返回类型K<V, Answer>对接起来,这里k(x).SelectMany(...)中的输入参数必须为Func<U, K<V, Answer>>,此时(1)的函数体可以写成如下,
return m.SelectMany<T, V, Answer>((T x) => k(x).SelectMany<U, V, Answer>(y => ...)
此时,这句代码中...部分的返回类型为K<V, Answer>,而要得到这个类型,此时已经很容易了,将s应用到x和y上,得到类型V的值,将V类型包装成K<V, Answer>即可,而包装可以使用ToContinuation<V, Answer>,故(1)的函数体为,
return m.SelectMany<T, V, Answer>((T x) => k(x).SelectMany<U, V, Answer>(y => s(x, y).ToContinuation<V, Answer>()));
此时,可以运行上面的那个LINQ的测试代码了。
深入理解Continuation Monad
Continuation Monad用于将内部的计算外置。举例说明,
var r = 7.ToContinuation<int, string>().SelectMany( x => 6.ToContinuation<int, string>().SelectMany( y => (x + y).ToContinuation<int, string>())); Console.WriteLine(r(i => i.ToString()));
很明显,这段测试代码用于将7和6相加,并将和转换为字符串。如果对7和6的和不直接简单的转换为字符串,而是想对和先加1再转换为字符串呢,此时只需要修改最后一句代码中int转换为string的逻辑即可,无需修改第一句程序语句。
Continuation Monad涉及两个类型之间的关系,实现将这两个类型之间的转换的逻辑实现外置,这与F#中的Continuation Computation类似。
我们在实现Continuation Monad的函数时,主要是通过函数参数对类型进行转换,直到获得需要的类型,如下面代码为例,用于将类型T转换为Answer类型(其中this所指参数省略),
public static K<T, Answer> CallCC<T, Answer>(this ...);
K<T, Answer>的类型参考上一篇文章。
K<T, Answer>正是用于将类型T转换为Answer类型的委托,那要得到这个返回类型,this所指的参数类型可以是Func<Func<T, K<U, Answer>>, K<T, Answer>>,这个类型函数的输入为一个从T到K<U, Answer>的Func,这个Func的输入必须为T,因为我们的最终目的是要从T转换为Answer,那自然必须提供一个T才行,当然,这个Func还可以是从T到K<T, Answer>,此时this所指参数类型为Func<Func<T, K<T, Answer>>, K<T, Answer>>。这个Func甚至可以是从T到K<T, U>的函数,可以有很多可能,这里仅列出已经提过的几种情况,
public static K<T, Answer> CallCC<T, U, Answer>(this Func<Func<T, K<U, Answer>>, K<T, Answer>> u); (1) public static K<T, Answer> CallCC<T, U, Answer>(this Func<Func<T, K<T, Answer>>, K<T, Answer>> u); (2)
public static K<U, Answer> CallCC<T, U, Answer>(this Func<Func<T, K<T, U>>, K<U, Answer>> u); (3)
再如第一种情况,this所指的参数类型Func<Func<T, K<U, Answer>>, K<T, Answer>>,这个类型函数的输出类型为K<T, Answer>正好与我们最终的返回类型相同,然而,如果这个类型函数的输出类型与最终输出类型不同呢?比如是K<U, Answer>,如下所示,
public static K<T, Answer> CallCC<T, U, Answer>(this Func<Func<T, K<U, Answer>>, K<U, Answer>> u); (4)
这种情况下,对提供的T,仅能得到K<U, Answer>类型,如果不提供从U到T的转换函数,则我们无法实现CallCC。
Continuation Monad函数实现
对第一种情况来说,参数u这个类型函数的输出类型即为我们所要的最终类型,故CallCC实现较为简单,如下
public static K<T, Answer> CallCC<T, U, Answer>(this Func<Func<T, K<U, Answer>>, K<T, Answer>> u) { return c => u(x => z => c(x))(c); }
第二种情况更加简单,可以看成是第一种情况的U为T的特殊情况。
第三种情况,参数u这个类型函数的输出类型也是最终的返回来下,其实现如下,
public static K<U, Answer> CallCC1<T, U, Answer>(this Func<Func<T, K<T, U>>, K<U, Answer>> u) { return c => u(t => z => z(t))(c); }
Continuation Monad函数用法
public void Call() { Func<Func<int, K<int, string>>, K<string, char[]>> u = U; var r = u.CallCC(); Console.WriteLine(r(s => s.ToCharArray())); } private K<string, char[]> U(Func<int, K<int, string>> k) { var i = 50; var m = 2; return k(m)(j => (i + j).ToString()).ToContinuation<string, char[]>(); }
其中,lambda表达式j => (i + j).ToString()中的j其实是m传入的。