正文
一、什么是函数式编程?
我的理解是函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数可以看成没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的。.函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用。函数式编程我觉得更贴近于数学思想或者说计算。
二、函数式编程的特性
1.函数是"第一等公民"
所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
举例来说,下面代码中的print变量就是一个函数,可以作为另一个函数的参数。
var print = function(i){ console.log(i);};
[1,2,3].forEach(print);
2.只用”表达式",不用"语句"
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。
当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。
3.没有"副作用"
所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
4.不修改状态
在函数式编程中,我们通常理解的变量在函数式编程中也被函数代替了:在函数式编程中变量仅仅代表某个表达式。这里所说的’变量’是不能被修改的。所有的变量只能被赋一次初值。(下面会详细介绍变量被函数代替)
5.引用透明性
函数程序通常还加强引用透明性,即如果提供同样的输入,那么函数总是返回同样的结果。就是说,表达式的值不依赖于可以改变值的全局状态。
三、函数式编程常用核心概念和技术
1.纯函数
什么是纯函数呢?
对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态的函数,叫做纯函数。
举个栗子:
var xs = [1,2,3,4,5];// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);// Array.splice会对原array造成影响,所以不纯
xs.splice(0,3);
2.函数柯里化(高级函数)
高阶函数,就是把函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
我的理解:传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
看一个例子:
// 柯里化之前
function add(x, y) {
return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
return function (x) {
return x + y;
};
}
addX(2)(1) // 3
柯里化可以理解为是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。
3.函数组合
为了解决函数嵌套过深,洋葱代码:h(g(f(x))),我们需要用到“函数组合”,我们可以
来用柯里化来改他,让多个函数像拼积木一样。
函数编程的函数组合:两个纯函数组合之后返回了一个新函数,举个例子:
const compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
const toUpperCase = function(x) {
return x.toUpperCase();
};
const exclaim = function(x) {
return x + "!";
};
const shout = compose( exclaim , toUpperCase );
console.log(shout("hello world")); //HELLO WORLD!
可以对他简化一下:
const compose=(f,g)=>(x=>f(g(x)))
const toUpperCase=x=>x.toUpperCase()
const exclaim=x=>x+'!'
const shout=compose(exclaim,toUpperCase)
console.log(shout('hello world')); //HELLO WORLD!
compose中的参数的顺序是随意的,这就类似于乘法中的交换律 xy=yx**,
所以函数式编程贴近于数学计算。
4.递归与尾递归
指函数内部的最后一个动作是函数调用。 该调用的返回值, 直接返回给函数。 函数调用自身, 称为递归。 如果尾调用自身, 就称为尾递归。 递归需要保存大量的调用记录, 很容易发生栈溢出错误, 如果使用尾递归优化, 将递归变为循环, 那么只需要保存一个调用记录, 这样就不会发生栈溢出错误了。通俗点说,尾递归最后一步需要调用自身,并且之后不能有其他额外操作。
举个例子:我的理解为满足递归条件相当于捕鱼时先撒网,等达到递归边界即捕到鱼时,再返回自身及收网;尾递归就为当捕到鱼时鱼已经到你手里了,具体举个例子:
// 不是尾递归,无法优化
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
} //这里的递归当到达边界条件时会把值返回给上一个调用它的函数
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
} //ES6强制使用尾递归
// 这里的尾递归就是当到底边界条件时直接将结果 return 出来
我们看一下递归和尾递归执行过程:
递归:
function sum(n) {
if (n === 1) return 1;
return n + sum(n - 1);
}
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15 //递归非常消耗内存,因为需要同时保存很多的调用帧,这样,就很容易发生“栈溢出”
尾递归:
function sum(x, total) {
if (x === 1) {
return x + total;
}
return sum(x - 1, x + total);
}
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15
整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0)。这能有效的防止堆栈溢出。 在ECMAScript 6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。
5.惰性计算(求值)
在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。延迟的计算使你可以编写可能潜在地生成无穷输出的函数。因为不会计算多于程序的其余部分所需要的值,所以不需要担心由无穷计算所导致的 out-of-memory 错误。简单的来说就是:
按需索取,能不多做事,绝不多做
这里有自己实现的简单的惰性求值
用JavaScript写一个惰性求值的最简实现
惰性求值官方文档
官方文档
四、函数式编程总结
1.函数式编程中的每个符号都是 const 的,于是没有什么函数会有副作用。谁也不能在运行时修改任何东西,也没有函数可以修改在它的作用域之外修改什么值给其他函数继续使用。这意味着决定函数执行结果的唯一因素就是它的返回值,而影响其返回值的唯一因素就是它的参数。
2.函数式编程不需要考虑”死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。
3.函数式编程中所有状态就是传给函数的参数,而参数都是储存在栈上的。这一特性让软件的热部署变得十分简单。只要比较一下正在运行的代码以及新的代码获得一个diff,然后用这个diff更新现有的代码,新代码的热部署就完成了。