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

函数式编程中的模式

在本章中

  • 核心函数 Map、Bind、Where 和 ForEach
  • 介绍 functors 和 monads
  • 在不同的抽象层次上工作

  模式是一种可以应用于解决各种问题的解决方案。我们将在本章中讨论的patterns是简单的函数;这些函数在进行功能编码时无处不在,可以被看作是FP的核心功能。
  你可能已经熟悉了其中的一些函数,比如Where和Select,在IEnumerable中使用过它们。但你会发现,同样的操作可以应用于其他结构,从而建立起一种模式。 在本章中,我将用Option来说明这一点;其他的结构将在以后的章节中出现。
  像往常一样,我建议你在REPL中输入,看看如何使用这些核心函数(你需要导入LaYumba.Functional库,如前所述)。

4.1 将一个函数应用于一个结构的内部值

  第一个核心函数是Map。它接收一个结构和一个函数,并将该函数应用于结构的内部值。让我们从熟悉的案例开始,在这个案例中,有关的结构是一个IEnumerable。

4.1.1 将函数映射到序列上

  对IEnumerable的Map的实现可以写成如下。

清单 4.1 Map 将函数应用于给定 IEnumerable 的每个元素

public static IEnumerable<R> Map<T, R>(this IEnumerable<T> ts, Func<T, R> f){
	foreach (var t in ts)
		yield return f(t);
}

  Map通过对源列表中的每个元素应用一个函数T -> R,将一个T的列表映射到一个R的列表。注意,在这个实现中,由于使用了yield return语句,结果被打包成了一个IEnumerable。

关于命名的注意事项 在FP中,使用变量名称是很正常的,比如t代表一个T类型的值,ts代表T的集合,f(g,h,等等)代表一个函数。在为更具体的情况编码时,你可以使用更具描述性的名字,但当一个函数像Map一样通用时,你对值t或函数f真的一无所知,变量就有相应的通用名字。

  从图形上看,Map 可以如图 4.1 所示。

在这里插入图片描述
  我们来看一个简单的用法:

Func<int, int> times3 = x => x * 3;
Range(1, 3).Map(times3);
// => [3, 6, 9]

也许你认识到,这正是你在调用LINQ的Select方法时得到的行为。事实上,Map可以用Select来定义:

publicstatic IEnumerable<R> Map<T, R> (this IEnumerable<T> ts, Func<T, R> f) => ts.Select(f);

  这有可能更有效率,因为LINQ对Select的实现是针对IEnumerable的某些特定实现而进行的。 重点是,我将使用Map这个名字,而不是Select,因为Map是FP中的标准术语,但你应该把Map和Select视为同义词。

4.1.2 将一个函数映射到一个Option上

  现在让我们看看如何为不同的结构定义 Map:Option。 IEnumerable 的 Map 的签名是

(IEnumerable< T >, (T -> R)) -> IEnumerable< R >

  让我们按照这个模式,直接用Option来替换IEnumerable。

(Option< T >, (T -> R)) -> Option< R >

  这个签名说你有一个可能包含T的选项,以及一个从T到R的函数;你必须返回一个可能包含R的选项。你能想到一个实现吗?
  让我们来看看。如果选项是None,就没有T可用,你所能做的就是返回None。一个只处理None情况的实现应该是这样的:

public static Option<R> Map<T, R> (this Option.None _, Func<T, R> f) => None;

  这个实现没有做任何事情:你从None开始,最后是None,而给定的函数f被忽略了。
  另一方面,如果选项是Some,那么它的内部值是一个T,所以你可以对它应用给定的函数,得到一个R,然后你可以把它包在Some中。因此,Some情况下的实现是这样的:

public static Option<R> Map<T, R> (this Option.Some<T> some, Func<T, R> f) => Some(f(some.Value));

  正如你在下面的列表中所看到的,实际的实现迎合了给定 Option 的两种可能的状态。

清单 4.2 选项映射的定义

public static Option<R> Map<T, R> (this Option<T> optT, Func<T, R> f) => optT.Match( () => None, (t) => Some(f(t)));

  如果给定的选项是 None,Map 就会在 None 状态下返回一个预期返回类型的 Option。 如果是Some(t),其中t是包装好的值,它将把t送入给定的函数(T -> R),然后把结果值提升到一个新的Option。你可以在图 4.2 中看到这一点。

在这里插入图片描述
  直观地说,把 Option 看作是一种特殊的列表,它既可以是空的(None),也可以正好包含一个值(Some)。如果你从这个角度看,就会发现Option和IEnumerable的Map的实现是一致的:给定的函数被应用于结构中所有可用的内部值。
  我们来看一个简单的例子:

Func<string, string> greet = name => $"hello, {name}";

Option<string> _ = None;
Option<string> optJohn = Some("John");

_.Map(greet);// => None
optJohn.Map(greet); // => Some("hello, John")

  这里有一个现实生活中的比喻:你有一个可爱的老阿姨,她的专长是做苹果派。她不喜欢去购物,但她喜欢烤馅饼(单一责任原则)。
  你经常在上班的路上把一篮苹果放在她门外,晚上你会发现一篮新鲜的馅饼!你的阿姨也很有幽默感,所以如果你变聪明,在她门口留下一个空篮子,你会发现一个空篮子回来。
在这个比喻中,篮子代表Option。苹果是输入选项的内在值,而你阿姨的烹饪技巧是应用到这个内在值的函数。 Map是将苹果拆箱,交给阿姨加工,再将烤好的馅饼重新装箱的过程。

class Apple { }
class ApplePie { public ApplePie(Apple apple) { } }

Func<Apple, ApplePie> makePie = apple => new ApplePie(apple);

Option<Apple> full  = Some(new Apple());
Option<Apple> empty = None;

full.Map(makePie)  // => Some(ApplePie)
empty.Map(makePie) // => None
4.1.3 Option 如何提升抽象级别

  要认识到的一件非常重要的事情是 Option 抽离了一个值是否存在的问题。如果你直接将一个函数应用到一个值上,你必须以某种方式确保这个值是可用的。相反,如果你把这个函数映射到一个Option上,你就不会关心这个值是否存在,Map会根据情况应用这个函数。
  这一点在目前可能还不完全清楚,但随着你对本书的阅读,会变得很清楚。 现在,让我们看看我是否能说明这个想法。 在第三章中,我们定义了一个基于年龄计算风险的函数,如下所示:

Risk CalculateRiskProfile(Age age) => (age.Value < 60) ? Risk.Low : Risk.Medium;

  现在,假设你正在做一项调查,人们自愿提供一些个人信息,并收到一些统计数据。 调查对象是用一个 Subject 类来建模的,定义如下:

class Subject{
	public Option<Age> Age { get; set; }
	public Option<Gender> Gender { get; set; }
	// many more fields...
}

  一些字段,如年龄,被建模为optional,因为调查者可以选择是否披露这一信息。
  这就是你如何计算一个特定 Subject 的风险:

Option<Risk> RiskOf(Subject subject) => subject.Age.Map(CalculateRiskProfile);

  因为风险是基于主体的年龄,而年龄是可选的,所以计算出来的风险也是可选的。你不必担心年龄是否存在;相反,你可以映射计算风险的函数,不管它是否存在,并通过返回包裹在Option中的结果来允许选择性的 “扩散”。
  接下来,让我们从更一般的角度来看 Map 模式。

4.1.4 介绍 functors

  你已经看到,Map是一个遵循精确模式的函数,它被用来将一个函数应用于一个结构的内部值,如IEnumerable或Option,以及其他许多结构,如集合、字典、树等等。
  让我们概括一下这个模式。 让C< T >表示一个通用的 “容器”,它包裹着一些T类型的内部值。那么Map的签名一般可以写成下面的样子:

Map : (C< T >, (T -> R)) -> C< R >

  也就是说,Map可以被定义为一个函数,它接收一个容器C< T >和一个类型为(T -> R)的函数f,并返回一个容器C< R >,该容器包裹着将f应用于容器内部的值。
  在FP中,为其定义了这样一个Map函数的类型被称为一个functor。IEnumerable和Option都是functor,正如你刚刚看到的,你将在书中遇到更多的functor。 为了实用起见,我们可以说,任何对Map有合理实现的东西都是一个functor。 但什么是合理的实现呢? 从本质上讲,Map应该对容器的内部值应用一个函数,同样重要的是,它不应该做其他事情;也就是说,Map不应该有副作用。

为什么是 functor 不是接口?
  如果Option和IEnumerable都支持Map操作,为什么我们不通过一个接口来捕获这个操作?事实上,这样做是很好的,但不幸的是,在C#中这是不可能的。为了说明原因,让我们试着定义这样一个接口:

interface Functor<F<>, T>
{
	F<R> Map<R>(Func<T, R> f);
}
public struct Option<T> : Functor<Option, T>
{
	public Option<R> Map<R>(Func<T, R> f) => // ...
}

  这不能编译:我们不能用F<>作为一个类型变量,因为与T不同,它并不表示一个类型,而是一种类型;也就是说,一个类型又被一个通用类型所参数化。而且,Map仅仅返回一个Functor是不够的;它必须返回一个与当前实例相同的Functor。
  其他语言(包括Haskell和Scala)支持所谓的 “高等类型”,因此有可能用类型库来表示这些更通用的接口,但在C#(和F#)中,我们必须满足于较低的抽象水平,并遵循基于模式的方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值