了解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

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的行为正确并能良好的组合。

Identity 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(), x =>
Bind(Unit(), 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 = .ToIdentity().SelectMany(
x => .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 = .ToIdentity()
.SelectMany(x => .ToIdentity(), (x, y) => x + y); Console.WriteLine(r.Value);

使用这个定义,C#程序员们则可以使用查询理解语法,如下

var r = from x in .ToIdentity()
from y in .ToIdentity()
select x + y;

记住,查询语法中使用的是后一个版本的SelectMany。

Maybe Monad

前面介绍的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 .ToMaybe()
from y in Maybe<int>.Nothing
select x + y; Console.WriteLine(r.HasValue ? r.Value.ToString() : "Nothing");

List Monad

还有一个重要的包装类型是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[] { , ,  }
from y in new[] { , , }
select x + y; foreach (var i in r)
Console.WriteLine(i);

Continuation Monad

如何更好的写出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 .ToContinuation<int,string>()
from y in .ToContinuation<int,string>()
select x + y; Console.WriteLine(r(z => z.ToString().Replace('', 'a'))); // displays a3

注:以上这段测试代码还无法通过编译,提示SelectMany函数的参数不对,如果对前面Identity Monad真正理解了的话,应该知道原因是什么。这里先不处理,下一篇关于Monads的实现中会解决这个问题。

三、Monad的魔力

对包装类型的组合需要monads。Identity,Maybe和IEnumerablemonads作为包装类型证明了monads的强大功能。Continuation monad,K,说明了monad是如何表现复合计算的。

C# Monads的实现(一)的更多相关文章

  1. [Javascript] Monads

    Monads allow you to nest computations. They are a pointed functor that adds mjoin and chain function ...

  2. C# Monads的实现(二)

    再谈continuation monad 上一篇中我们已经介绍了continuation monad,但是这个monad与Identity,Maybe,IEnumerable monads稍微难于理解 ...

  3. C# 函数式编程及Monads.net库

    函数式编程中,一切皆为函数,这个函数一般不是类级别的,其可以保存在变量中,可以当做参数或返回值,是函数级别的抽象和重用,将函数作为可重用的基本模块,就像面向对象中一切皆为对象,把所有事物抽象为类,面向 ...

  4. 深圳scala-meetup-20180902(3)- Using heterogeneous Monads in for-comprehension with Monad Transformer

    scala中的Option类型是个很好用的数据结构,用None来替代java的null可以大大降低代码的复杂性,它还是一个更容易解释的状态表达形式,比如在读取数据时我们用Some(Row)来代表读取的 ...

  5. 【函数式】Monads模式初探——for解析式

    for表达式是monad语法糖 先看一组演示样例: case class Person(name: String, isMale: Boolean, children: Person*) val la ...

  6. 【函数式】Monads模式初探——Endofunctor

    自函子 自函子(Endofunctor)是一个将范畴映射到自身的函子(A functor that maps a category to itself). 函子是将一个范畴转换到另一个范畴.所以自函子 ...

  7. RAC学习笔记

    RAC学习笔记 ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架,Cocoa是苹果整套框架的简称,因此很多苹果框架喜欢以Cocoa结尾. 在学习Re ...

  8. monads-are-elephants(转)

    介绍monads有点像互联网时代的家庭手工业.我想 “为什么要反对传统?”,但这篇文章将以Scala对待monads的方式来描述. 有个古老的寓言,讲述了几个瞎子第一次摸到大象.一个抱着大象的腿说:“ ...

  9. Functions

    Small The first rule of functions is that they should be small.The second rule of functions is that ...

随机推荐

  1. scp命令报错(IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!)

    使用scp命令移动某一服务器的文件到另外的服务器时报出一下错误: [root@bogon vm_sys1]# scp project.tar.gz root@172.31.0.90:/webdata/ ...

  2. 9.XML文件解析

    一.XML简介 XML(EXtensible Markup Language),可扩展标记语言 特点:XML与操作系统.编程语言的开发平台无关 实现不同系统之间的数据交换 作用:数据交互 配置应用程序 ...

  3. CSS菜单横竖布局要点

    菜单纵向:把ul 元素的边框属性去除,li元素用border-top  上边框显示分离,把a 元素用display:block  text-decoration:none  去除默认下划线 菜单横向: ...

  4. android移动开发学习笔记(二)神奇的Web API

    本次分两个大方向去讲解Web Api,1.如何实现Web Api?2.如何Android端如何调用Web Api?对于Web Api是什么?有什么优缺点?为什么用WebApi而不用Webservice ...

  5. 浙大 pat 1038 题解

    1038. Recover the Smallest Number (30) 时间限制 400 ms 内存限制 32000 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHE ...

  6. asp.net时间范围查询

    1.首先要查询类表中的一个时间段,要明确我的数据库中只有一个时间字段,我们先将他拆分一下. if ($("#news_OpenTime").val() != "" ...

  7. KMP算法的实现

    今天看到了一篇关于KMP算法的讲解的文章,很难得,讲得非常清楚.分享给大家,希望对大家有帮助.http://kb.cnblogs.com/page/176818/ 我自己基于这个讲解的内容作了一个实现 ...

  8. WPF 命令的简单总结

    WPF的命令Command主要解决的问题,就是代码复用.一个很重要的应用意义,在于它将很多地方需要的调用的相同操作,以统一的方式管理,却又提供了不同的访问结果. 举个例子来说,我可能通过“点击butt ...

  9. nodejs 实现简单的文件上传功能

    首先需要大家看一下目录结构,然后开始一点开始我们的小demo. 文件上传总计分为三种方式: 1.通过flash,activeX等第三方插件实现文件上传功能. 2.通过html的form标签实现文件上传 ...

  10. 【git】TurtoiseGit使用手册

    A,创建本地分支 操作:Create Branch ->输入本地分支名 -> OK B,创建本地分支和远程分支的映射 操作: 1,拉取远程分支:Fetch -> 选中Arbitray ...