了解Haskell语言的朋友都知道它是一门纯函数式编程,输出结果只跟输入参数相关,这导致Haskell不能有输入输出函数,因为不同的环境下,输入相同,但输出可能有所不同。Haskell语言中,变量的值一旦被赋值,则不会再被修改,所以这也导致了它就无法输出——因为输出会改变屏幕。那Haskell中如何实现输入输出功能?
Haskell中输入输出的功能是通过Monad方法实现的。本篇不讨论Haskell语言,只讨论基于C#下的Monad实现。
主要参考资料
Monads来自范畴论。在一篇优秀的论文The Essence of Functional Programming中,Wadler展示了monads在计算程序中非常有用,因为能够组合不同的函数,而这些函数具有不同的对数值处理的逻辑。
Monads在Haskell语言中获得非常大的成功,跟在特殊领域能发挥很好作用的actor一样,monads已经深入程序员脑海,普遍认为它在纯延迟函数式语言中才有用。这是很不幸的,事实上monads应用可以更宽广。
组合是软件中控制复杂度的关键点。在 The Structure and Interpretation of Computer Programs中,Abelson和Sussman认为好的组合能把复杂的系统已简单的模式显示出来。
其中一种组合为函数组合,简单地描述了函数调用之间的依赖关系。比如,将两个函数作为输入参数,并把第二个函数的结果值作为第一个函数的输入参数,因此形成一个函数如下
public static Func<T, V> Compose<T, U, V>(this Func<U, V> f, Func<T, U> g) { return x => f(g(x)); }
如果不是先将函数g应用到值x上,然后对函数g的结果值应用函数f,那么,可以先组合函数f和函数g,将组合的结果应用到值x上,这两种方式的关键区别是函数f和g之间依赖关系的抽象
var r = f(g(x)); // without function composition var r = f.Compose(g)(x); // with function composition
假设有如下Identity函数,那函数组合必须遵循以下三条规则
public static T Identity<T>(this T value) { return value; }
1. 左恒等律
Identity.Compose(f) = f
2. 右恒等律
f.Compose(Identity) = f
3. 结合律
f.Compose(g.Compose(h)) = (f.Compose(g)).Compose(h)
当然,不仅仅是值,结构化类型扩展了值类型。如,IEnumerable<T>表示延时计算的T类型列表,Nullable<T>表示类型T的可为空(null)值,而Func<Func<T, Answer>, Answer>表示一个函数,这个函数有一个continuation输入,并返回一个Answer,而这个continuation则有一个T类型参数并返回Answer。这些类型都扩展了T类型。
这里约定,M<T>类型扩展了类型T,并且我们统一称之为T的包装类型,为M<T>,而T称为未包装类型。
假定不是组合返回类型为T的函数,而是组合这样一种函数,这些函数以未包装类型为输入参数,返回包装类型,如下代码所示
public static Func<T, M<V>> Compose<T, U, V>(this Func<U, M<V>> f, Func<T, M<U>> g) { return x => f(g(x)); // error, g(x) returns M<U> and f takes U }
这个组合会失败,原因如代码注释所说。我们增加一个"Bind"函数用以访问M<U>的去包装类型U,并将这个值传递给f函数,如下
public static Func<T, M<V>> Compose<T, U, V>(this Func<U, M<V>> f, Func<T, M<U>> g) { return x => Bind(g(x), f); }
根据输入和输出可以决定Bind函数的签名,g函数的输出为M<U>,故Bind函数签名为
public static M<V> Bind<U, V>(this M<U> m, Func<U, M<V>> k)
除了Bind函数,我们还可以定义包装一个未包装类型参数使其成为包装类型,称此函数为"Unit",
public static M<T> Unit<T>(this T value)
Bind函数和Unit函数可以使用包装类型进行函数组合。
Monads由三部分组成:类型、Unit函数以及Bind函数。当然要成为一个monad,这三部分必须满足以下三条定律:
1. 左恒等律
Bind(Unit(e), k) = k(e)
2. 右恒等律
Bind(m, Unit) = m
3. 结合律
Bind(m, x => Bind(k(x), y => h(y)) = Bind(Bind(m, x => k(x)), y => h(y))
第一条和第二条定律显然易见故不再赘述。
第三条定律其实也容易理解,“=”左边是外层Bind函数对m进行去包装化,内层Bind函数对k(x)进行去包装化,而“=”右边是内层Bind函数对m进行去包装化,外层Bind函数对k(x)进行去包装化。
这些定律与函数组合类似。这不是一个巧合。这些定律保证了monad的行为正确并能良好的组合。
最简单的Monad是Identity monad,用于包装一个值。
class Identity<T> { public T Value { get; private set; } public Identity(T value) { this.Value = value; } }
有了类型T和T的包装类型Identity之后,我们可以上面那些只有函数签名没有函数体的函了。
Unit函数输入一个T类型值,返回Identity的实例,如下
static Identity<T> Unit<T>(T value) { return new Identity<T>(value); }
Bind函数有一个Identity类型参数,去包装这个参数为一个值,然后调用第二个参数,第二个参数是个委托类型,将去包装后的值传入这个委托,结果是Identity的另一个实例,如下
static Identity<U> Bind<T,U>(Identity<T> id, Func<T,Identity<U>> k) { return k(id.Value); }
考虑这样一个简单的程序:创建两个Identity包装器,并对包装类型执行一个操作。首先,绑定x到5的包装类型上,然后绑定y到6的包装类型上。最后,将5和6相加,结果是11的包装类型,如下
var r = Bind(Unit(5), x => Bind(Unit(6), y => Unit(x + y))); Console.WriteLine(r.Value);
当然,这段代码看起来很不简洁,最好能有一个处理monad的语法。幸运的是,事实上真有这样的语法。
C#3.0引入了查询理解(query comprehensions),其本质上是monad理解(comprehensions)。我们可以使用LINQ重写identity monad。也许,可以把它称之为LINM(Language INtegrated Monads)。
把Unit重命名为ToIdentity,把Bind重命名为SelectMany,这两个重命名后的函数均为扩展方法,如下
public static Identity<T> ToIdentity<T>(this T value) { return new Identity<T>(value); } public static Identity<U> SelectMany<T, U>(this Identity<T> id, Func<T, Identity<U>> k) { return k(id.Value); }
于是,刚才的使用代码可以改写为如下
var r = 5.ToIdentity().SelectMany( x => 6.ToIdentity().SelectMany( y => (x + y).ToIdentity())); Console.WriteLine(r.Value);
与此相对应的方法正是LINQ中定义的标准查询操作符。然而,为了性能考虑,标准查询操作符还包含了一个稍有不同的SelectMany版本。这个SelectMany版本组合Bind和Unit,所以lambda不需要嵌入的很深,不像上面这段代码。先给出这个版本的SelectMany的方法定义,看看能从中发现什么。
public static Identity<V> SelectMany<T, U, V>(this Identity<T> id, Func<T, Identity<U>> k, Func<T,U,V> s) { return id.SelectMany(x => k(x).SelectMany(y => s(x, y).ToIdentity())); }
这个SelectMany版本的定义中有SelectMany的重载版本,这个重载版本可参见上一个SelectMany方法定义。
当然,我们也可以利用已有的代码改写这个版本的SelectMany为如下
public static Identity<V> SelectMany<T, U, V>(this Identity<T> id, Func<T, Identity<U>> k, Func<T,U,V> s) { return s(id.Value, k(id.Value).Value).ToIdentity(); }
于是,调用代码不需要嵌套的lambda表达式了,
var r = 5.ToIdentity() .SelectMany(x => 6.ToIdentity(), (x, y) => x + y); Console.WriteLine(r.Value);
使用这个定义,C#程序员们则可以使用查询理解语法,如下
var r = from x in 5.ToIdentity() from y in 6.ToIdentity() select x + y;
记住,查询语法中使用的是后一个版本的SelectMany。
前面介绍的Identity仅是对一个类型的最简单的monad化包装。如果我们需要某包装类型,这个包装类型中包含的值可能为一个值,也可能为一个空值(null),此时我们就可以使用Maybe monad。
首先定义Maybe类型,如下
class Maybe<T> { public readonly static Maybe<T> Nothing = new Maybe<T>(); public T Value { get; private set; } public bool HasValue { get; private set; } Maybe() { HasValue = false; } public Maybe(T value) { Value = value; HasValue = true; } }
相应的Unit函数有一个未包装类型输入参数,构造一个Maybe的实例并返回,
public static Maybe<T> ToMaybe<T>(this T value) { return new Maybe<T>(value); }
相应的Bind函数有一个Maybe类型输入参数,如果这个参数有值,则将这个值传入Bind的委托类型参数,否则返回Nothing,
public static Maybe<U> SelectMany<T, U>(this Maybe<T> m, Func<T, Maybe<U>> k) { if (!m.HasValue) return Maybe<U>.Nothing; return k(m.Value); }
下面给出调用示例
var r = from x in 5.ToMaybe() from y in Maybe<int>.Nothing select x + y; Console.WriteLine(r.HasValue ? r.Value.ToString() : "Nothing");
还有一个重要的包装类型是list类型。事实上,list monad是LINQ的核心。IEnumerable<T>表示延迟计算list。
相应的Unit函数为,
public static IEnumerable<T> ToList<T>(this T value) { yield return value; }
Bind函数则有两个输入参数,分别为IEnumerable<T>类型和委托类型,这个委托类型有一个输入参数类型T,返回类型为IEnumerable<U>,最终Bind函数的返回类型为IEnumerable<U>,
public static IEnumerable<U> SelectMany<T, U>(this IEnumerable<T> m, Func<T, IEnumerable<U>> k) { foreach (var x in m) foreach (var y in k(x)) yield return y; }
现在,给出一个使用示例,
var r = from x in new[] { 0, 1, 2 } from y in new[] { 0, 1, 2 } select x + y; foreach (var i in r) Console.WriteLine(i);
如何更好的写出CPS(Continuation-Passing Style)代码?
Continuation monad, K, 的类型是一个委托类型,当给定一个continuation,这个continuation本身有一个T类型输入参数并返回Answer类型结果,那么,委托最终返回Answer类型,签名如下
delegate Answer K<T,Answer>(Func<T,Answer> k);
注意,这个类型与上面所说的Identity<T>,Maybe<T>和IEnumerable<T>都不同。上面的那些monads表示了一个类型的包装类型,计算也是针对未包装类型而不是包装类型。但是,这个continuation monad与众不同,它不包含值,它所做的事情就是将用户写的continuation组合起来。
要成为一个monad,必须有相应的Unit函数,这个函数输入参数为T,返回参数为T的包装类型,即K<T, Answer>,签名如下
public static K<T, Answer> ToContinuation<T, Answer>(this T value);
分析这个函数签名,输入参数为T类型,输出为另一个函数(参见上面的委托签名),称为F,这个函数F自身的参数为Func<T, Answer>,形式如下
return (Func<T, Answer> c) => ...
根据上面的那个委托类型,故这个F函数的返回类型为Answer,其所需的T类型参数正是value值,故F函数体可以写成如下
return (Func<T, Answer> c) => c(value);
为了成为一个monad,Bind函数必须有一个K<T, Answer>输入参数,以及一个函数参数,这个函数用于从T类型转换到K<U, Answer>类型,最终Bind函数的返回类型为K<U, Answer>,函数签名如下
public static K<U, Answer> SelectMany<T, U, Answer>(this K<T, Answer> m, Func<T, K<U, Answer>> k);
但是函数体如何实现?返回类型必须是K<U, Answer>,但是这个返回类型如何形成?
我们再次看看SelectMany的组成(将K类型展开):
返回类型为K,K是一个输入为Func<U, Answer>,输出为Answer的函数,故返回类型如下,
Func<Func<U, Answer>, Answer>
m的类型也为K,只是泛型参数从U变为T,m类型如下,
Func<Func<T, Answer>, Answer>
k的类型,是一个函数,输入为T,输出为K,可以写成其类型如下
Func<T, Func<Func<U, Answer>, Answer>>
分析:
将k应用到T上返回一个类型K<U, Answer>的值,但是T类型值从哪里来?首先,根据返回类型K<U, Answer>可以构造出SelectMany函数体类似如下,
return (Func<U,Answer> c) => ...
根据K的类型可以推断return关键字后面的lambda函数体的返回类型必须为Answer,而m可以应用到一个从T转为Answer的函数上,m的返回类型正好为Answer,如此,SelectMany函数体类似如下,
return (Func<U,Answer> c) => m(...)
m调用的内部表达式必须是Func<T, Answer>,目前还没有这个类型,所以我们构造这样一个函数类型,即创建一个参数为T的lambda表达式,如下,
return (Func<U,Answer> c) => m((T x) => ...)
m调用的内部lambda表达式的函数体返回类型为Answer,所以我们现在要做的事情就是如何得到这个Answer类型。
考虑如果应用k到x上,则返回类型为Func<Func<U, Answer>, Answer>,要想得到类型Answer类型,则将这个返回结果再次应用到Func<U, Answer>上即可。前面我们已经有Func<U, Answer>的参数c,故现在SelectMany函数体可写成如下,
return (Func<U,Answer> c) => m((T x) => k(x)(c));
构建一个计算,调用值为7的continuation,将这个continuation传入另一个计算,这个计算调用值为6的continuation。再讲这个continuation传入另一个计算,这个计算调用的计算用来将前两个continuation的结果值相加。最后,传入一个continuation,用于将"1"替换为"a",如下
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
注:以上这段测试代码还无法通过编译,提示SelectMany函数的参数不对,如果对前面Identity Monad真正理解了的话,应该知道原因是什么。这里先不处理,下一篇关于Monads的实现中会解决这个问题。
对包装类型的组合需要monads。Identity,Maybe和IEnumerablemonads作为包装类型证明了monads的强大功能。Continuation monad,K,说明了monad是如何表现复合计算的。