简单说,"函数式编程"是一种"编程范式"(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
从上面代码执行的结果看到,我们追踪某个函数的执行结果是没有侵入性的,不需要更改原函数的任何代码。我们定义到组合函数就像管道一样,把输入的值按顺序经过一个个管道进行处理并流向下一个管道。我们可以追踪到每个管道处理的结果,这就是组合函数的一个优势。