函数式编程常用术语
2017-02-11 20:56
281 查看
近年来函数式编程这种概念渐渐流行起来,尤其是在React/Vuejs这两个前端框架的推动下,函数式编程就像股新思潮一般瞬间席卷整个技术圈。虽然博主接触到的前端技术并不算深入,可这并不妨碍我们通过类似概念的延伸来理解这种概念。首先,函数式编程是一种编程范式,而我们所熟悉的常见编程范式则有命令式编程(Imperative Programmming)、函数式编程(Functional Programming)、逻辑式编程(Logic Programming)、声明式编程(Declarative Programming)和响应式编程(Reactive Programming)等。现代编程语言 在发展过程中实际上都在借鉴不同的编程范式,比如Lisp和Haskell 是最经典的函数式编程语言,而SmartTalk、C++和Java则是最经典的命令式编程语言。微软的C#语言最早主要借鉴Java语言,在其引入lambda和LINQ特性以后,使得C#开始具备实施函数式编程的基础,而最新的Java8同样开始强化lambda这一特性,为什么lambda会如此重要呢?这或许要从函数式编程的基本术语开始说起。
这是使用Python编写的函数式编程风格的代码,或许看到这样的代码,我们内心是完全崩溃的,可是它实现得其实是这样一个功能,即将集合{0, 1, 2, 3, 4}中的每个元素进行平方操作,然后返回一个新的集合。如果使用命令式编程,我们注定无法使用如此简单的代码实现这个功能。而这个功能在.NET中其实是一个Select的功能:
这就是函数式编程的魅力,我们所做的事情都是由一个个函数来完成的,这个函数定义了输入和输出,而我们只需要将数据作为参数传递给函数,函数会返回我们期望的结果。好了,下面再看一个例子:
即使我们从来没有了解过函数式编程,从命名我们依然可以看出这是一个对集合中的元素求和的功能实现,这就是规范命名的重要性。幸运的是.NET中同样有类似的扩展方法,我喜欢Linq,我喜欢lambda:
考虑到博主写不出更复杂的函数式编程的代码示例,这里不再列举更多的函数式编程风格的代码,可是我们从直观上来理解函数式编程,就会发现函数式编程同lambda密不可分,函数在这里扮演着重要的角色。好了,下面我们来了解下函数式编程中的常用术语。
Map函数需要一个元素集合和一个访问该元素集合中每一个元素的函数,该函数将生成一个新的元素集合,并返回这个新的元素集合。通过C#中的迭代器可以惰性实现Map函数:
Filter函数需要一个元素集合和一个筛选该元素结合的函数,该函数将从原始元素集合中筛选中符合条件的元素,然后组成一个新的元素集合,并返回这个新的元素集合。通过C#中的Predicate委托类型,我们可以写出下面的代码:
Fold函数实际上代表了一系列函数,而最重要的两个例子是左折叠和右折叠,这里我们选择相对简单地左折叠来实现累加的功能,它需要一个元素集合,一个累加函数和一个初始值,我们一起来看下面的代码实现:
相信现在大家应该理解什么是高阶函数了,这种听起来非常数学的名词,当我们尝试用代码来描述的时候会发现非常简单。相信大家都经历过学生时代,临近期末考试的时候死记硬背名词解释的情形,其实可以用简洁的东西描述清楚的概念,为什么需要用这种方式来理解呢?为什么我这里选择了C#中的委托来编写这些示例代码呢?自然是同样的道理啦,因为我们都知道,在C#中委托是一种类似函数指针的概念,因为当我们需要传入和返回一个函数的时候,选择委托这种特殊的类型可谓是恰如其分啦,这样并不会影响我们去理解高阶函数。
这是一个由匿名方法定义的委托类型,显然我们需要在调用这个方法前准备好两个参数x和y,这意味着C#不允许我们在改变参数列表的情况下调用这个方法。而通过局部套用:
实际上在这里两个参数x和y的顺序对最终结果没有任何影响,我们这样写仅仅是为了符合人类正常的认知习惯,而此时我们注意到我们在调用curriedAdd时会发生质的的变化:
而如果我们将这里的函数用Lambda表达式来表示,则会发现:
至此,对一般的局部套用,存在:
则称后者为前者的局部套用形式。
因为在这里表达式的第一部分返回值为false,因此在实际调用中第二部分根本不会执行,因为无论第二部分返回true还是false,实际上对整个表达式的结果都不会产生影响。这是一个非常经典的非严格求值的例子,同样的,布尔运算中的”||”运算符,同样存在这个问题。所以,至此我们可以领会到惰性求值的优点,即使程序的执行效率更好,尤其是在避免高昂运算代价的时候,我们要牢记:懒惰是程序员的一种美德,使用更简洁的代码来满足需求,是一名游戏程序员的永恒追求。我们可以联想那些在代码片段中优先return的场景,这大概勉强可以用这种理论来解释吧!例如我们强大的Linq,原谅我如此执著于举Linq的例子,Linq的一个特点是当数据需要被使用的时候开始计算,即数据是延迟加载的,而在此之前我们所有对数据的操作,从某种意义上来讲,更像是定义了一系列函数,这好像和数据库中的事务非常相近啦,其实这就是在告诉我们,懒惰是一种美德啊,哈哈!
什么是函数式编程?
我们提到函数式编程是一种编程范式,它的基本思想是将计算机运算当作是数学中的函数,同时避免了状态和变量的概念。一个直观的理解是,在函数式编程中面向数据,函数是第一等公民,而我们传统的命令式编程中面向过程,类是第一等公民。为什么我们反复提到lambda呢?因为函数式编程中最重要的基础是lambda演算(Lambda Calculus),并且lambda演算的函数可以接受函数作为参数和返回值,这听起来和数学有关,的确函数式编程是面向数学的抽象,任何计算机运算在这里都被抽象为表达式求值,简而言之,函数式程序即为一个表达式。值得一提的是,函数式编程是图灵完备的,这再次说明数学和计算机技术是紧密联系在一起的。虽然在博主心目中认为,图灵这位天纵英才的英国数学家,是真正的计算机鼻祖,但历史从来都喜欢开玩笑的,因为现代计算机是以冯.诺依曼体系为基础的,而这一体系天生就是面向过程即命令式的,在这套体系下计算机的运算实则是硬件的一种抽象,命令式程序实际上是一组指令集。因此,函数式程序目前依然需要编译为该体系下的计算机指令来执行,这听起来略显遗憾,可这对我们来说并不重要,下面让我们来一窥函数式编程的真容:squares = map(lambda x: x * x, [0, 1, 2, 3, 4]) print squares
这是使用Python编写的函数式编程风格的代码,或许看到这样的代码,我们内心是完全崩溃的,可是它实现得其实是这样一个功能,即将集合{0, 1, 2, 3, 4}中的每个元素进行平方操作,然后返回一个新的集合。如果使用命令式编程,我们注定无法使用如此简单的代码实现这个功能。而这个功能在.NET中其实是一个Select的功能:
int[] array = new int[]{0, 1, 2, 3, 4}; int[] result = array.Select(m => m * m).ToArray();
这就是函数式编程的魅力,我们所做的事情都是由一个个函数来完成的,这个函数定义了输入和输出,而我们只需要将数据作为参数传递给函数,函数会返回我们期望的结果。好了,下面再看一个例子:
sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4]) print sum
即使我们从来没有了解过函数式编程,从命名我们依然可以看出这是一个对集合中的元素求和的功能实现,这就是规范命名的重要性。幸运的是.NET中同样有类似的扩展方法,我喜欢Linq,我喜欢lambda:
int[] array = new int[]{0, 1, 2, 3, 4}; int result = array.Sum();
考虑到博主写不出更复杂的函数式编程的代码示例,这里不再列举更多的函数式编程风格的代码,可是我们从直观上来理解函数式编程,就会发现函数式编程同lambda密不可分,函数在这里扮演着重要的角色。好了,下面我们来了解下函数式编程中的常用术语。
函数式编程的常用术语
函数式编程首先是一种编程范式,这意味着它和面向对象编程一样,都是一种编程的思想。而函数式编程最基本的两个特性就是不可变数据和表达式求值。基于两个基础特性,我们延伸出了各种函数式编程的相关概念,而这些概念就是函数式编程的常用术语。常用的函数式编程术语有高阶函数、柯里化/局部调用、惰性求值,递归等。在了解这些概念前,我们先来理解,什么是函数式编程的不可变性。不可变性,意味着在函数式编程中没有变量的概念,即操作不会改变原有的值而是修改新产生的值。举一个基本的例子,.NET中IEnumerable接口提供了大量的如Select、Where等扩展方法,而这些扩展方法同样会返回IEnumerable类型,并且这些扩展方法不会改变原来的集合,所有的修改都是作用在一个新的集合上,这就是函数式编程的不可变性。实现不可变性的前提是纯函数,即函数不会产生副作用。一个更为生动的例子是,如果我们尝试对一个由匿名类型组成的集合进行修改,会被提示该匿名类型的属性为只读属性,这意味着数据是不可改变的,如果我们要坚持对数据进行“修改”,唯一的方法就是调用一个函数。高阶函数(Higer-Order-Function)
高阶函数是指函数自身能够接受函数,并返回函数的一种函数。这个概念听起来好像非常复杂的样子,其实在我们使用Linq的时候,我们就是在使用高阶函数啦。这里介绍三个非常有名的高阶函数,即Map、Filter和Fold,这三个函数在Linq中分别对应于Select、Where和Sum。我们可以通过下面的例子来理解:Map函数需要一个元素集合和一个访问该元素集合中每一个元素的函数,该函数将生成一个新的元素集合,并返回这个新的元素集合。通过C#中的迭代器可以惰性实现Map函数:
IEnumerable<R> Map<T,R>(Func<T,R> func, IEnumerable<T> list) { foreach(T item in list) yield return func(item); }
Filter函数需要一个元素集合和一个筛选该元素结合的函数,该函数将从原始元素集合中筛选中符合条件的元素,然后组成一个新的元素集合,并返回这个新的元素集合。通过C#中的Predicate委托类型,我们可以写出下面的代码:
IEnumerable<T> Filter<T>(Predicate<T> predicate, IEnumerable<T> list) { foreach(T item in list) { if(predicate(item)) yield return item; } }
Fold函数实际上代表了一系列函数,而最重要的两个例子是左折叠和右折叠,这里我们选择相对简单地左折叠来实现累加的功能,它需要一个元素集合,一个累加函数和一个初始值,我们一起来看下面的代码实现:
R Fold<T,R>(Func<R,T,R> func, IEnumerable<T> list, R startValue = default(R)) { R result = startValue; foreach(T item in list) result = func(result, item); return result; }
相信现在大家应该理解什么是高阶函数了,这种听起来非常数学的名词,当我们尝试用代码来描述的时候会发现非常简单。相信大家都经历过学生时代,临近期末考试的时候死记硬背名词解释的情形,其实可以用简洁的东西描述清楚的概念,为什么需要用这种方式来理解呢?为什么我这里选择了C#中的委托来编写这些示例代码呢?自然是同样的道理啦,因为我们都知道,在C#中委托是一种类似函数指针的概念,因为当我们需要传入和返回一个函数的时候,选择委托这种特殊的类型可谓是恰如其分啦,这样并不会影响我们去理解高阶函数。
柯里化(Curring)/局部套用
柯里化(Curring)得名于数学家Haskell Curry,你的确没有看错,这位伟大的数学家不仅创造了Haskell这门函数式编程语言,而且提出了局部套用(Currin)这种概念。所谓局部套用,就是指不管函数中有多少个参数,都可以函数视为函数类的成员,而这些函数只有一个形参,局部套用和部分应用息息相关,尤其是部分应用是保证函数模块化的两个重要技术之一(部分应用和组合(Composition)是保证函数模块化的两个重要技术)。众所周知,在C#中一个函数一旦完成定义,那么它的参数列表就是确定的,即相对静态。它不能像Python和Lua一样去动态改变参数列表,虽然我们可以通过缺省参数来减少参数的个数,可是在大多数情况下,我们都需要在调用函数前准备好所有参数,而局部套用所做的事情与这个理念截然相反,它的目标是用非完全的参数列表去调用函数。我们来一起看下面这个例子:Func<int,int,int> add = (x,y) => {return x + y;};
这是一个由匿名方法定义的委托类型,显然我们需要在调用这个方法前准备好两个参数x和y,这意味着C#不允许我们在改变参数列表的情况下调用这个方法。而通过局部套用:
Func<int,int,int> curriedAdd => (x) => { return (y) => { return x + y;}; };
实际上在这里两个参数x和y的顺序对最终结果没有任何影响,我们这样写仅仅是为了符合人类正常的认知习惯,而此时我们注意到我们在调用curriedAdd时会发生质的的变化:
//x和y同时被传入add add(x,y) //x和y可以不同时被传入curriedAdd curriedAdd(x)(y);
而如果我们将这里的函数用Lambda表达式来表示,则会发现:
Func<int,int,int> add = (x,y) => return x + y; Func<int,Fucn<int,int>> curriedAdd = x = > y => x + y;
至此,对一般的局部套用,存在:
Func<...> f = (part1, part2, part3, ...) => ... 可转换为: Func<...> cf = part1 => part2 => part3 ... => ...
则称后者为前者的局部套用形式。
惰性求值
我们在前文中曾经提到过,在函数式编程中函数是第一等公民,而这里的函数更接近数学意义上的函数,即将函数视为一个可以对表达式求值的纯函数,所以我们这里自然而然地就提到了惰性求值。首先,博主这里想说说求值策略这个问题,求值策略通常有严格求值和非严格求值两种,而对C#语言来讲,它在大多数情况下使用严格求值策略,即参数在传递给函数前求值。与之相对应的,我们将参数在传递给函数前不进行求值或者延迟求值的这种情况,称为非严格求值策略。一个经典的例子是C#中的“短路”效应:bool isTrue = (10 < 5) && (MyCheck())
因为在这里表达式的第一部分返回值为false,因此在实际调用中第二部分根本不会执行,因为无论第二部分返回true还是false,实际上对整个表达式的结果都不会产生影响。这是一个非常经典的非严格求值的例子,同样的,布尔运算中的”||”运算符,同样存在这个问题。所以,至此我们可以领会到惰性求值的优点,即使程序的执行效率更好,尤其是在避免高昂运算代价的时候,我们要牢记:懒惰是程序员的一种美德,使用更简洁的代码来满足需求,是一名游戏程序员的永恒追求。我们可以联想那些在代码片段中优先return的场景,这大概勉强可以用这种理论来解释吧!例如我们强大的Linq,原谅我如此执著于举Linq的例子,Linq的一个特点是当数据需要被使用的时候开始计算,即数据是延迟加载的,而在此之前我们所有对数据的操作,从某种意义上来讲,更像是定义了一系列函数,这好像和数据库中的事务非常相近啦,其实这就是在告诉我们,懒惰是一种美德啊,哈哈!