c#函数式编程 Functional Programming in C# [4]
1.4 高阶函数
现在你已经了解了什么是FP,我们也回顾了语言的功能特性,现在是时候开始探索一些具体的功能技术了。我们将从函数作为第一公民的最重要的好处开始:它使你有能力定义高阶函数(HOFs)。
HOFs是将其他函数作为输入或将一个函数作为输出返回的函数,或者两者都是。我假定你已经在某种程度上使用过HOFs,比如用LINQ。在本书中,我们会经常使用HOFs,所以本节应作为复习,并可能介绍一些你可能不太熟悉的HOFs用例。HOFs很有趣,本节中的大多数例子都可以在REPL中运行。请确保你在这一过程中尝试一些自己的变化。
1.4.1 依赖于其他函数的函数
一些 HOF 将其他功能作为参数并调用它们以完成其工作,有点像一家公司可能会将其某些工作分包给另一家公司。您已经在本章前面看到了一些此类 HOF 的示例:Sort(List 上的实例方法)和 Where( 上的扩展方法)。
List.Sort,当与委托一起调用时,是一个说:"好吧,我自己排序,只要你告诉我如何比较我包含的任何两个元素。"Sort做排序的工作,但调用者可以决定使用什么逻辑进行比较。
同样地,Where做的是过滤的工作,调用者决定用什么逻辑来判断一个元素是否应该被包含。你可以用图形来表示Where的类型,如图1.4所示。
让我们看一下 Where 的理想化实现
清单1.10 Where:一个典型的HOF,反复应用给定的谓词。
publicstatic IEnumerable<T> Where<T> (this IEnumerable<T> ts, Func<T, bool> predicate)
{foreach (T t in ts)if (predicate(t))yield return t;
}
Where方法负责排序逻辑,调用者提供,这是应该被过滤的标准。
正如你所看到的,HOF可以在逻辑不容易分离的情况下帮助实现关注点的分离。Where和Sort是迭代应用的例子–HOF将对集合中的每个元素重复应用给定的函数。
一个非常粗略的方法是,你传递的参数是一个函数,其代码最终将在HOF中的循环体中执行–这是你只传递静态数据所做不到的。一般的方案显示在图1.5中
可选执行是HOF的另一个好办法。 当你想只在特定的条件下调用一个给定的函数时,这很有用,如图1.6所示。
例如,设想有一个方法可以从缓存中查找一个元素。可以提供一个委托,并在缓存丢失的情况下被调用。
清单 1.11 可选调用给定函数的 HOF
class Cache < T > where T: class {public T Get(Guid id) => //...public T Get(Guid id, Func < T > onMiss) => Get(id) ?? onMiss();
}
中的逻辑可能涉及到一个昂贵的操作,如数据库调用,你不会希望这个操作被不必要地执行。
前面的例子说明了将一个函数作为输入的HOF(通常被称为回调或延续),并使用它来执行一个任务或计算一个值。这也许是HOF最常见的模式,它有时被称为控制倒置:HOF的调用者通过提供一个函数来决定做什么,而被调用者通过调用给定的函数来决定何时做。
让我们看看 HOF 派上用场的其他一些场景。
1.4.2 适配器功能
有些HOF根本不应用给定的函数,而是返回一个新的函数,与作为参数的函数有某种联系。例如,假设你有一个执行整数除法的函数:
Func<int, int, int> divide = (x, y) => x / y;
divide(10, 2) // => 5
你想改变参数的顺序,使除数排在前面。 这可以看作是一个更普遍的问题的一个特殊案例:改变参数的顺序。
你可以写一个通用的HOF,通过调换参数的顺序来修改任何二进制函数。
static Func<T2, T1, R> SwapArgs<T1, T2, R>(this Func<T1, T2, R> f)=> (t2, t1) => f(t1, t2);
从技术上讲,更正确的说法是 返回一个新的函数,该函数以相反的顺序调用给定的函数的参数。但从直观的角度看,我发现认为我得到的是原始函数的一个修改版本更容易。
现在你可以通过应用来修改原来的除法函数:
var divideBy = divide.SwapArgs();
divideBy(2, 10) // => 5
在玩这种HOF的时候,我想到了一个有趣的想法,那就是函数并不是固定不变的:如果你不喜欢一个函数的接口,你可以通过另一个函数来调用它,这个函数提供了一个更适合你需要的接口。这就是为什么我把这些函数称为适配器。
1.4.3 创建其他函数的函数
有时你写的函数的主要目的是创建其他函数–你可以把它们看作是函数工厂。 下面的例子使用一个来过滤一个数字序列,只保留能被2整除的数字:
var range = Enumerable.Range(1, 20);
range.Where(i => i % 2 == 0)
// => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
如果你想要更普遍的东西,比如能够过滤被任何数字n整除的数字,会怎么样?你可以定义一个函数,接收n并产生一个合适的谓词,来评估任何给定的数字是否能被n整除:
Func<int, bool> isMod(int n) => i => i % n == 0;
我们以前没有看过这样的HOF:它接收一些静态数据并返回一个函数。让我们看看你如何使用它:
using static System.Linq.Enumerable;
Range(1, 20).Where(isMod(2)) // => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Range(1, 20).Where(isMod(3)) // => [3, 6, 9, 12, 15, 18]
请注意,你不仅在通用性方面得到了提高,而且在可读性方面也得到了提高 在这个例子中,你使用isMod HOF来产生一个函数,然后你把它作为输入送给另一个HOF,Where,如图1.7所示
你会在书中看到更多的HOF的用法。最终你会把它们看成是常规函数,而忘记了它们是高阶函数。现在让我们来看看它们如何在更接近日常开发的情况下使用。