简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。
它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。
FP和指令式编程相比,函数式编程的思维方式更加注重函数的计算。它的主要思想是把问题的解决方案写成一系列嵌套的函数调用。
就像在OOP中,一切皆是对象,编程的是由对象交合创造的世界;在FP中,一切皆是函数,编程的世界是由函数交合创造的世界。
函数式编程中最古老的例子莫过于1958年被创造出来的Lisp了。Lisp由约翰·麦卡锡(John McCarthy,1927-2011)在1958年基于λ演算所创造,采用抽象数据列表与递归作符号演算来衍生人工智能。较现代的例子包括Haskell、ML、Erlang等。现代的编程语言对函数式编程都做了不同程度的支持,例如:JavaScript, Coffee Script,PHP,Perl,Python, Ruby, C# , Java 等等(这将是一个不断增长的列表)
对于JavaScript来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此JavaScript中函数是一等公民。
var name = 'jack'
function sayHi(name) {}
var hello = sayHi
var hi = () => sayHi
function ajax(cb) {
cb()
}
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
副作用让一个函数变得不纯是有道理的:从定义上来说,纯函数必须要能够根据相同的输入返回相同的输出;如果函数需要跟外部事物打交道,那么就无法保证这一点。
「相同输入得到相同输出」
.
var xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3);
//=> [1,2,3]
// 不纯的
xs.splice(0,3);
//=> [1,2,3]
splice改变了原数组,使得相互引入共同的变量的地方潜藏着危险。slice不会改变原变量,接近于纯函数。
纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。
纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 缓存 技术:
var squareNumber = cache(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16
cache实现
function cache(fn) {
var cache = Object.create(null);
return function() {
var args = Array.prototype.slice.call(arguments);
//通过拼接参数形成一个独一无二的键值对key
var str = args.join("-");
// 当有缓存的时候直接取缓存的,没缓存则只需执行fn函数进行处理并缓存
return cache[str] || (cache[str] = fn.apply(fn, arguments));
};
}
把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
// 普通的add函数
function add(x, y) {
return x + y
}
// Currying后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
// 正常正则验证字符串 reg.test(txt)
// 函数封装后
function check(reg, txt) {
return reg.test(txt)
}
check(/\d+/g, 'test') //false
check(/[a-z]+/g, 'test') //true
// Currying后
function curryingCheck(reg) {
return function(txt) {
return reg.test(txt)
}
}
var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)
hasNumber('test1') // true
hasNumber('testtest') // false
hasLetter('21212') // false
如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);
shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"
compose(f, compose(g, h))
// 等同于
compose(compose(f, g), h)
// 等同于
compose(f, g, h)
场景 输入一个数字对其进行 +10 *10 +2
// 1
const add10 = num => num + 10
// 2
const multiply10 = num => num * 10
// 3
const add2 = num => num + 2
const disposeInput = input => add2(multiply10(add10(input)))
let num = disposeInput(2)
console.log('num', num)
// 函数组合 compose
const compose = (...fns) => x => fns.reduce((v, f) => f(v), x)
const dispose1 = compose(add10, multiply10, add2)
let num1 = dispose1(2)
console.log('num1', num1)
从上面的例子,我们看到disposeInput函数嵌套的写法让人抓狂,函数从内向外执行,如果真的用到我们的业务代码中,那将是一块难以维护的一块代码,我们实现一个从左到右执行到函数compose, 而下面经过组合的函数dispose1则显的清晰的多,而且更容易维护。组合函数可不止给人这种清晰易维护的感觉,下面的代码示例则展示如何追踪代码执行的结果。
对组合的函数进行追踪
const dispose2 = compose(
trace('input'),
add10,
trace('add10'),
multiply10,
add2,
trace('end')
)
let num2 = dispose2(5)
console.log('num2', num2)
// == input: 5
// == add10: 15
// == end: 152
// num2 152
从上面代码执行的结果看到,我们追踪某个函数的执行结果是没有侵入性的,不需要更改原函数的任何代码。我们定义到组合函数就像管道一样,把输入的值按顺序经过一个个管道进行处理并流向下一个管道。我们可以追踪到每个管道处理的结果,这就是组合函数的一个优势。