c#函数式编程 Functional Programming in C# [21]

用函数组合设计程序

本章包括

  • 用函数组合和方法链来定义工作流
  • 编写组合良好的函数
  • 使用工作流处理服务器请求的端到端示例

  函数组合不仅功能强大,表现力强,而且工作起来也很愉快。它在某种程度上被用于任何编程风格,但在FP中,它被广泛地使用。例如,你是否注意到,当你使用LINQ来处理列表时,只需几行代码就能完成很多工作?这是因为LINQ是一个功能性的API,在设计时考虑到了组合。
  在这一章中,我们将介绍函数编译的基本概念和技术,并说明它在LINQ中的应用。 我们还将实现一个端到端的服务器端工作流,其中我们将使用第四章中介绍的Option API。 这个例子将说明函数式方法的许多想法和好处,所以我们将在本章结束时讨论这些问题。

5.1 函数组合

  让我们首先回顾一下函数组合以及它与方法链的关系。函数组合是任何程序员隐含知识的一部分。 这是一个你在学校里学到的数学概念,然后每天都在使用它,而不需要考虑太多,所以让我们快速了解一下这个定义。

5.1.1 重温函数组合

  给定两个函数 f 和 g,你可以定义一个函数 h 为这两个函数的组合,记作如下:

h = f · g

  将h应用于一个值x,与将g应用于同一个值x以获得一个中间结果,然后将f应用于该中间结果是一样的。就是说。

h(x) = (f · g)(x) = f(g(x))

  例如,假设你想得到一个在 Manning(出版社名) 工作的人的电子邮件地址。你可以让一个函数计算本地部分(识别人),另一个函数附加域名:

static string AbbreviateName(Person p)
	=> Abbreviate(p.FirstName) + Abbreviate(p.LastName);
static string AppendDomain(string localPart)
	=> $"{localPart}@manning.com";
static string Abbreviate(string s)
	=> s.Substring(0, 2).ToLower();

  AbbreviateName和AppendDomain是两个函数,你可以通过编译得到一个新的函数,该函数将产生我假设的合作者的 Manning(出版社名) 的电子邮件。

清单 5.1 将一个函数定义为两个现有函数的组合

Func<Person, string> emailFor =
	p => AppendDomain(AbbreviateName(p)); // emailFor 是 AppendDomain 和 AbbreviateName 的组合。
var joe = new Person("Joe", "Bloggs");
var email = emailFor(joe);
// => jobl@manning.com

  有几件事情值得注意。首先,你只能用匹配的类型来组合函数:如果你要组合(f · g),g的输出必须是可分配给f的输入类型。
  第二,在函数组合中,函数的出现顺序与执行顺序相反。 例如,在AppendDomain(AbbreviateName§)中,你首先执行最右边的函数,然后是它左边的一个。 当然,这对于可读性来说并不理想,尤其是当你想对几个函数进行组合时。
  与其他语言不同,C#对函数组合没有任何特殊的语法支持,尽管你可以定义一个HOF 组合来组合两个或多个函数,但这并不能提高可读性。 这就是为什么在C#中通常会采用方法链的方式。

5.1.2 方法链

  方法链语法(即用.操作符链式调用几个方法)提供了一种在C#中实现函数组合的更可读的方法。给定一个表达式,你可以将任何被定义为表达式类型的实例或扩展方法的方法链到它上面。
  例如,前面的示例需要修改如下:

//添加this关键字使其成为扩展方法。emailFor是AppendDomain和AbbreviateName的组合。Maps组合函数
static string AbbreviateName(this Person p)
	=> Abbreviate(p.FirstName) + Abbreviate(p.LastName);
static string AppendDomain(thisstring localPart)
	=> $"{localPart}@manning.com";

  您现在可以链接这些方法来获取此人的电子邮件

清单 5.2 使用方法链语法来组合函数

var joe = new Person("Joe", "Bloggs");
var email = joe.AbbreviateName().AppendDomain();
// => jobl@manning.com

  请注意,现在扩展方法是按照它们将被执行的顺序出现的。这极大地提高了可读性,尤其是当工作流的复杂性增加时(长的方法名,额外的参数,更多的方法被链起来),这也是为什么方法链是C#中实现函数组合的最好方式。

5.1.3 提升世界的构成

  函数的构成是如此重要,以至于它在提升值的世界中也应该成立。 让我们继续使用当前确定一个人的电子邮件地址的例子,但是现在我们将有一个 Option作为起始值。你会认为下面的情况是成立的:

//emailFor 是 AppendDomain 和 AbbreviateName 的组合。
Func<Person, string> emailFor =
	p => AppendDomain(AbbreviateName(p));
var opt = Some(new Person("Joe", "Bloggs"));
//映射组合函数
var a = opt.Map(emailFor);
//在不同的步骤中映射AbbreviateName和AppendDomain
var b = opt.Map(AbbreviateName)
	.Map(AppendDomain);
Assert.AreEqual(a, b);

  也就是说,无论你是在不同的步骤中映射AbbreviateName和AppendDomain,还是在一个步骤中映射它们的组合emailFor,结果都不应该改变,而且你应该能够在这两种形式之间安全地重构。
  更一般地说,如果h = f · g,那么将h映射到一个 functor 上应该等同于将g映射到该 functor 上,然后将f映射到结果上。这应该对任何 functor 和任何函数对都成立,这是 functor 定律之一,所以任何Map的实现都应该遵守它。
  如果这听起来很复杂,那可能是因为它描述了一些你直觉上应该始终坚持的东西。事实上,打破这个定律并不容易,但是你可以想出一个恶作剧的 functor ,比如说,保持一个内部计数器来记录 Map 被应用了多少次(或者每次调用 Map 时改变它的状态),然后前面的说法就不成立了,因为b的内部计数会比a的大。
  简单地说,Map应该将一个函数应用于 functor 的内部值,而不是其他,因此,在处理functor 时,函数组合就像处理正常值一样。这样做的好处是,你可以在任何编程语言中使用任何函数式库,并且可以放心地使用任何函数式,例如在前面的片段中改变a和b之间的关系,这样的重构是安全的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值