什么是函数式编程
从 λ演算 说起
λ演算是图灵完备的,它是一种通用的计算模型,可用于模拟任何一台单带图灵机。图灵完备意味着你的语言可以做到能够用图灵机能做到的所有事情,可以解决所有的可计算问题。
λ演算是一个极其简单但又十分强大的概念。它的核心主要有两个概念:
- 函数的抽象,通过引入变量来归纳得出表达式
- 函数的应用,通过给变量赋值来对已得出的表达式进行计算
举个栗子,我们表示一个一元二次函数是这样子:
1 | y = x^2-2*x+1 |
如果用λ演算表示,会变成这样的形式:
1 | // λ<变量>.<表达式> |
而当我们把1赋值给x时,它的调用过程会是这样:
1 | // x=1 |
如果是二元一次函数,调用过程会变成这样:
1 | // x=1 y=2 |
从上面可以看出,λ演算有2个特点:
- 计算顺序是固定的,从里层到外层一层层归约,如果改变变量的次序也会影响函数应用中的返回值。
- 函数的返回值也可以是一个函数,这与函数式编程语言中函数是一等公民的概念是一致的。
定义
说完λ演算,我们就可以来看下函数式编程的定义:
函数式编程(functional programming)或称函数程序设计,又称泛函编程,是一种编程典范,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是λ演算(lambda calculus)。
举一个简单的例子:用函数式编程实现if else
1 | // IF(cond)(thn)(els) |
通过上面的例子,从数据的角度看函数式编程,在数据流动过程中,数据的形式是通过函数进行变换的。
编程范式
函数式编程是一种范式,大家还可能听说过下面的这些方式:
- Object-oriented (OOP) 面向对象
- Procedural 面向过程
- Functional 函数式
- Imperative 声明式
- …
而这些编程范式的关系可以参考下面这张图:
编程范式的好处在于:易于在学校教学,引发讨论,区分编程语言。
命令式(imperative)语言是指一步步导向目标的一个过程。程序的执行往往限制在某些状态上。状态有global/local variable,它们是可以变化的。还有if else等control flow的结构。命令式语言常见的有C, C++,Java等等。
声明式(declarative)语言描述目标,具体的执行由引擎,系统等等决定。function calls, high order functions, recursion等都是它的特点。比较常见的语言包括:Scala,Haskell,Lisp,Scheme等。
实际上大部分语言都是混合的,代码的风格不是由一小块代码决定,而是要从整体构架的角度出发。而Javascript是非纯函数式编程语言里最早在mainstream里加入函数式编程特性的语言。
为什么要函数式编程
数据和行为的关系
在计算机中,数据多数指的是存储结构。行为指的多数是计算操作。而在不同编程范式里,它们的关系会有所不同。
我们先来看看面向对象式:
1 | // man.php |
通过上面的例子可以看出,在面向对象的编程中,我们习惯把对象作为行为的核心,也就是说,先有人,然后,人来执行一个动作。而,对象,其实就是某一种变量,亦或是某一种数据类型。
然后我们再看看函数式:
1 | function Man(sexy){ |
在函数式编程中,我们去除掉了主语。你不知道这个动作是由谁发出的。相比于在面向对象编程中,数据是对象的属性,在函数式编程中,我们并不在乎这个数据的内容是什么,而更在乎其变化。
在实际的开发过程中,我们有的时候很难抽象出一个对象来描述我们到底要做什么,或者说,我们其实并不在乎这堆数据里面的内容是什么,我们要关心的,只是把数据经过加工,得出结果,仅此而已。至于这个数据,到底是用来干什么的,我们其实可以并不用关心。
函数式编程的特性
一等公民
在函数式编程中,函数是一等公民:
函数可以存储为变量
函数可以成为数组的一个元素
函数可以成为对象的成员变量
函数可以在使用的时被直接创建
函数可以被作为实参传递
函数可以被另一个函数返回
函数可以返回另一个函数
函数可以作为形参
简单地说,所谓一等公民,说的是函数本身可以成为代码构成中的任意部分。
函数式编程的特性
纯函数
什么是纯函数,我们可以看下面的例子:
1 | var xs = [1,2,3,4,5]; |
可以看出,所谓的纯函数就是不产生任何的副作用的函数。
可组合
当函数纯化之后,有一个很鲜明的特点是,这个函数变的可以组合了。我们可以像堆乐高积木一样,把各个我们要用的函数堆起来变成一个更大得函数体。
1 | /** |
高阶函数
1 | function calc(x){ |
高阶函数就是以一个函数为参数,同时返回一个函数作为函数的返回值。
1 | // redux-thunk |
函数式编程的好处
1. 代码简洁,开发快速
函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。
2. 接近自然语言,易于理解
函数式编程的自由度很高,可以写出很接近自然语言的代码。
3. 更方便的代码管理
函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。
4. 易于”并发编程”
函数式编程不需要考虑”死锁”(deadlock),因为它不修改变量,所以根本不存在”锁”线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署”并发编程”(concurrency)。
如何进行函数式编程
柯里化
定义
在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
Curry化来源于数学家 Haskell Curry的名字 (编程语言 Haskell也是以他的名字命名)。柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程。
一个简单的例子:求和函数
1 | var sum = function (a, b, c) { |
1 | var sumCurrying = function(a) { |
柯里化的函数每次调用都返回一个新的函数,该函数接受另一个调用,然后又返回一个新的函数,直至最后返回结果,分布求解,层层递进。
更通用的柯里化:
1 | var currying = function (fn) { |
柯里化的作用
- 延迟计算
从上面的例子可以看出。
- 参数复用
当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选。
- 动态创建函数
这可以是在部分计算出结果后,在此基础上动态生成新的函数处理后面的业务,这样省略了重复计算。
高阶组件
一般的高阶组件使用形式:1
const EnhancedComponent = higherOrderComponent(WrappedComponent);
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
高阶组件: connect
1 | import { connect } from 'react-redux' |
1 | function connect(mapStateToProps, mapDispatchToProps) { |
Ramda
ramdajs是一个更具有函数式代表的JavaScript库。
我们可以快速看下它的使用情景:
1 | // 判断第一个参数是否大于第二个参数 |
与 loadash 等函数式工具库不同的是,ramda 把处理的数据放到了第一个参数Ramda 的数据一律放在最后一个参数,理念是”function first,data last”。除了数据放在最后一个参数,Ramda 还有一个特点:所有方法都支持柯里化。由于这两个特点,使得 Ramda 成为 JavaScript 函数式编程最理想的工具库。
Ref :