上一篇中介绍了如何使用amplified type, 如IEnumerable<T>,如果我们能找到组合amplified type函数的方法,就会更容易写出强大的程序. 我们已经说了很多次函数组合, 听起来又干又硬。函数组合其实就是简单编程,当我们写像下面这样的代码时:
var customer=customerData.GetById(customerId); var order=customer.CreateOrder();
我就是在使用函数组合,在这个例子里组合了GetById和CreateOrder函数, 所有的程序都是一个函数组合,显示的或者隐式的.
作为程序员我们知道怎么样组合参数和返回值都是unamplified type的函数,这很自然.
这里有两个简单的函数. 他们有相同的函数签名,我会使用函数委托和lambda表达式 ,而不是直接写静态方法:
Func<int,int>add2=x=>x+2; Func<int,int>mult2=x=>x*2;
组合这两个函数很简单:
Func<int,int>add2mult2=x=>mult2(add2(x));
我们使用新函数:
var r2=add2mult2(5); Console.Out.WriteLine("r2={0}",r2);
结果输出r2=14
我们一直在做这样的操作,但是如果我们要组合返回amplified type的函数呢。首先我们需要一个amplified type来组合,在这个例子里我们选择最简单的amplified type. 它仅封装了一个值,我们定义它为Identity:
public class Identity<T> { public T Value {get;set;} public Identity(T value) { Value=value; } }
现在我们创建两个返回amplified type的函数
Func<int,Identity<int>>add2=x=>new Identity(x+2); Func<int,Identity<int>>mult2=x=>new Identity(x*2);
如果我们用上面同样的方式组合新函数,编译会失败:
Func<int,Identity<int>>add2mult2=x=>mult2(add2(x));
因为add2返回Identity<int>类型,而multi2的参数是int,类型不匹配,所以编译失败。我们当然可以使用Value属性,
Func<int,Identity<int>>add2mult2=x=>mult2(add2(x).Value)
这样可以,但问题是们要组合Identity<T>的任何函数时都要写x.Value.不要忘了Identity<T>是我们可以想到的最简单的amplified type,实际上它一点用也没有。我们如果要使用有用的amplified type,如IEnumerable<T>,Task<T>或者IMightThrowAnException<T>,我们就又回到了要添加很多样板代码的老路上。我们需要去掉x.Value,怎样去做呢?
我们要得到的是一个函数,它组合了add2和mult2, 我们定义这个函数为Bind。看看上面编译失败的例子,我们就知道Bind的签名了。它需要接收add2的返回值,一个Identity<T>类型的值和mult2,一个签名Func<int, Identity<int>>的函数. 为了使Bind更有用,我们定义它为泛型函数,并且是扩展方法:
//a function ‘Bind‘,allows us to compose Identity returning functions public static Identity<B> Bind<A,B>(this Identity<A>, Func<A, Identity<B>>func) { func(a.Value); }
当然Bind函数体简单的调用了a.Value并且将结果传给了func. 我们用Bind封装了x.Value. 这样的好处是这种处理amplified type的机制依赖于amplified type封装的类型。Bind的签名是唯一的.
现在我们可以组合add2和mult2了
Func<int,Identity<int>>add2Mult2=x=>add2(x).Bind(mult2);
我们可以使用我们新组合生成的函数:
var r1=add2mult2(5); Console.Out.WriteLine("r1.Value={0}",r1.Value);
输出r1=14
让我们创建另一个有用的扩展方法, ToIdentity, 为了创建一个新的Identity:
public static Identity<T>ToIdentity<T>(T value) { return new Identity<T>(value); }
现在我们可以用各种Identity值写复杂的表达式:
var result="Hello World!".ToIdentity().Bind(a=> 7.ToIdentity().Bind(b=> (new DateTime(2010,1,11)).ToIdentity().Bind(c=> (a+", "+b.ToString()+", "+c.ToShortDateString()) .ToIdentity()))); Console.WriteLine(result.Value);
输出Hello World!, 7, 11/01/2010
这里我们接收一个string ,"Hello World!",把它转换成一个Identity<string>.将整数7转换为Identity<int>, 日期转换为Identity<DateTime>.我们用Bind函数巧妙的访问封装的值, 连接它们并返回一个Identity<string>.
上面的code不是你见过的最优美的, 但是再看一下,我们接收了不同类型的amplified type, 不用访问Value 属性就可以对它们进行操作, 这是关键. 记住两个事实:首先在未来我们可以去掉lambda表达式,其次Identity<T>可能是最简单的amplified type了. 我们可以实现任意复杂度的功能.
Identity<T>是一个Monad,Monad要求有一个type constructor(Identity<T>自己 )和连个方法:我们的Bind和ToIdentity, 这就是全部。你会发现Bind和ToIdentity在不同语言里有不同的名字。ToIdentity在Haskell中 被叫做return,Bind在C#里被叫做SelectMany, 稍后会看到。他们叫什么名字并不重要,重要的是签名正确:
Bind: Func<Identity<A>,Func<A,Identity<B>>,Identity<B>>
ToIdentity:Func<T,Identity<T>>
Bind是我们写Monad功能的地方, 并且是唯一的地方,这是非常强大的抽象,也是Monad最有魅力的地方.
下一篇我们会看到怎样把Bind重命名为SelectMany, 并与ToIdentity组合使用,使我们可以在Linq中使用Identity<T> Monad