创建我们第一个Monad

Posted 哨兵

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了创建我们第一个Monad相关的知识,希望对你有一定的参考价值。

        上一篇中介绍了如何使用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

以上是关于创建我们第一个Monad的主要内容,如果未能解决你的问题,请参考以下文章

介绍一个简单的Parser

学习函数式编程 Monad

创建自己的代码片段(CodeSnippet)

函数式编程-将Monad(单子)融入Swift

寻求对 monad 实施的建设性批评

Monad的重点