Skip to main content

函数式编程进阶

函数式编程思维

范畴论

函数式编程是范畴论的数学分支,是一门很复杂的数学,认为世界上所有概念体系都可以抽象出一个个范畴。彼此之间存在某种关系概念、事务、对象等,都构成范畴。任何事物只要找出他们之间的关系就能定义。范畴成员之间的关系叫“态射”,范畴论认为,同一个范畴的所有成员就是不同状态的“变形”,通过“态射”一个成员可以变形成另一个成员。

Locale Dropdown

在编程中:

  1. 所有成员是一个集合
  2. 变形关系是函数

函数式编程基础理论

函数式编程中的函数有以下几个关键点

  1. 函数必须总是接受一个参数
  2. 函数必须返回一个值
  3. 函数返回值和内部运算应该依赖接收到的参数,而不是外部运行环境
  4. 固定的输入对应固定的输出

函数式编程不是用函数来编程,也不是传统的面向对象编程。主旨在于将复杂的函数分解简单的函数(计算理论、递归论、拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用。同时需要区分函数和方法,方法要与指定的对象绑定,函数可以直接调用。

随着React的高阶函数,函数式编程开始变得火热。在JS中函数是一等公民,所谓“一等公民”指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数或者作为别的函数的返回值。

不可改变量,在函数式编程中通常理解的变量在函数式编程中也被函数代替了,在函数式编程中变量仅仅代表某个表达式,这里所说的变量是不能被修改的,所有的变量只能被赋值一次。 map和reduce是常用的函数式编程的方法

函数式编程常用核心概念

专业术语

纯函数

对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。

//纯函数
import _form 'lodash'
const sin = _.memorize(x => Math.sin(x))
//第一次计算的时候会稍慢点
const a = sin(7);
//第二次有了缓存,速度极快
const b =sin(7)

纯函数的优点: 纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性。

纯度和幂等性

幂等性是指执行无数粗后还具有相同的结果,相同的参数运行一次函数和运行两次结果一致。幂等性在函数式编程中与纯度相关但又不一致。

Math.abs(Math.abs(-42))
偏应用函数

传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。偏函数之所以“偏”就在于其职能处理那些能至少匹配一个case语句的输入,而不能处理所有可能的输入。

函数的柯里化

柯里化通过偏应用函数实现,它是把一个多参数函数转换为一个嵌套一元函数的过程。传递跟函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

const chacker = min => age => age>min;
const chacker18 = chacker(18);
checker18(20)

函数柯里化实现

const curry = (fn, arr= [])=>(...args)=> 
(arg=>(args.length === fn.length ? fn(...arg) : curry(fn,arg)))([
...arr,
...args
])

函数的反柯里化和函数柯里化正好相反,扩大适用范围,创建一个应用范围更广的函数,使本来只有特定对象才适用的方法扩展到更多的对象。

Function.prototype.uncurring = function(){
const self = this;
return function(){
const obj = Array.prototype.shift.call(arguments);
return self.apply(obj, arguments)
}
}

const push = Array.prototype.push.unCurrying(), obj = {};
push(obj, 'first', 'second');

柯里化优缺点:事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数得到一个已经记住这些参数的新函数,某种意义上来讲这是对参数的一种“缓存”,是一种非常高效的编写函数的方法

函数组合

为了解决纯函数嵌套的问题,引入函数组合的概念,compose函数只能组合接受一个参数的函数,类似于filter、map接受两个参数(投影函数:总是在应用转换操作,通过传入高阶函数后返回数组),不能直接组合可以借助偏函数包过后继续组合。函数组合的数据流是从右到左,以你为最右边的函数首先执行,将数据传递给下一个函数依次类推。通过组合子管理程序的控制流。

函数组合子

命令式代码能够通过if-else和for来控制执行流程但是函数式则不能,所以需要使用函数组合子,组合子可以组合其他函数(或者其他组合子)并作为控制逻辑单元的高阶函数,组合子通常不声明任何变量也不包含任何业务逻辑旨在管理函数程序执行流程并立案时调用中对中间结果进行操作,常见的组合子如下:

  • 辅助组合子

    无为(nothing)、照旧(identity)、默许(defaultTO)、恒定(always)

  • 函数组合子

    收缩(gather)、展开(spread)、颠倒(reverse)、左偏(partial)、右偏(partialRight)、柯里化(curry)、弃离(tap)、交替(alt)、补救(tryCatch)、同时(seq)、聚集(converge)、映射(map)、分捡(useWith)、违约(reduce)、组合(compose)

  • 谓语组合子

    过滤(filter)、分组(group)、排序(sort)

  • 其他

    组合子变换(juxt)

  • 分属于SKI组合子
Point Free

把一些对象自带的方法转化为纯函数,不要命名中间变量。如下函数中使用了中间变量,这个中间变量除了让代码变得更长一点外毫无意义。

const toUperCase = word => word.toUpperCase();
const split = x => str=>str.split(x);
const f = compose(split(' '), toUpperCase);
f("abc def")

const f = str => str.toUpperCase().splice('')

这种风格能够帮助我们编码时减少不必要的命名,让代码保持简洁和通用

声明式与命令式代码

命令式代码的意思就是我们通过一条又一条指令去让计算机执行一些动作,其中一般都会涉及到很多繁杂的细节。而声明式代码就要优雅很多通过写表达式的方式来声明想要干什么,而不是通过一步一步的指示。

//命令式代码
let CEOs = [];
for(let i = 0;i<companies.length;i++>){
CEOs.push(companies[i].CEO);
}

//声明式
let CEOs = companies.map(c=> c.CEO);

优缺点:函数式编程的一个明显的好处就是使用这种声明式代码,对于无副作用的纯函数可以完全不用考虑函数内部是如何实现的,专注于编写业务代码。优化代码时目光主需要聚集在这些稳定坚固的函数内部即可。相反对于不纯的函数式代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些副作用,在复杂系的系统中对于编码者来说是极大的负担。

惰性链、惰性求值、惰性函数

_.chain(数据).map().reverse().value()惰性链可以添加一个输入对象的状态,从而能够将这些输入转换为所需的输出操作链接在一起。与简单的数组操作不一样,尽管它是一个复杂的程序,但仍然可以避免创建任何变量,并且有效消除所有循环,且在最后调用value之前并不会真正执行任何操作,这就是所谓的惰性链

当输入很大但是只有一个小的子集有效时,避免不必要的函数调用就是所谓的惰性求值,惰性求值方法有很多如组合子,目的是尽可能的推迟求值,直到以来的表达式被调用

const alt = _.curry((fun1, fun2,val) => fun1(val) || fun2(val))

const showStudent = _.compose(函数体1, alt(xx1,xx2));
showStudent({})

惰性函数:假如同一个函数被大量调用,并且这个函数内部又有很多判断来检测函数,这样对于一个调用会浪费时间和浏览器资源,当所有条件第一次判断完成后,直接把这个函数改写不再需要判断

const object = {a: 'xxx', b: 2};
const values = _.memoize(_.values);
values(object)
object.a = '石晓波';
console.log(values.cache.get(object))

案例

//_.chain可以推断可优化点 入合并执行或存储优化
//合并函数执行 并压缩计算过程中使用的零时数据结构降低内存占用
const trace = (msg) => console.log(msg);
let square = (x) => Math.ow(x,2);
let isEven = (x) => x%2 === 0;
//使用组合子跟踪
square = R.compose(R.top(() => trace('map数组)),square);
isEvent = R.compose(R.tap(() => trace('filter 数组)), isEvent);

const numbers._.range(200);
const result = _chain(numbers)
.map(square)
.filter(isEvent)
take(3)
.value();
console.log(result);

判断客户端信息使用惰性函数创建XHR

function createXHR(){
let xhr = null;
if(typeof XMLHttpRequest !== 'undefined'){
xhr = new XMLHttpRequest();
createXHR = function(){
//直接返回一个懒函数,这样就不必要每次都判断
return new XMLHTTPRequest();
}
} else {
try{
xhr = new ActiveObject('Msxml2.XMLHTTP');
createXHR = function(){
return new ActiveObject('Msxml2.XMLHTTP')
}
} catch(e){
try{
xhr = new ActiveObject('Microsoft.XMLHTTP');
createXHR = function(){
return new ActiveObject('Microsoft.XMLHTTP');
}
} catch(e){
createXHR = function(){
return null;
}
}
}
}
}

更加专业的术语

高阶函数

函数当参数,把传入的函数做一个封装,然后返回这个封装的函数达到更高程度的抽象。以一个函数为参数或/和以一个函数作为返回结果。

尾调用优化

函数内部的最后一个动作是函数调用,该调用的返回值直接返回给函数。函数调用自身称为递归,如果尾调用自身就称为尾递归,递归需要保存大量的调用记录很容易发生栈溢出错误,如果使用尾递归调用将递归变为循环,那么只需要保存一个调用记录就不会发生栈溢出错误。

//不是尾递归无法优化斐波那契数列
function factorial(n){
if(n === 1) return 1;
return n * functorial(n-1)
}

//使用尾递归优化
function factorial(n, total = 1){
if(n === 1 ) return total;

return factorial(n-1, n*total)
}

普通递归内存需要记录调用的堆栈所处的深度和位置信息。在最底层计算返回值再根据记录的信息跳回上一层级计算,然后再跳回更高一层依次运行,知道最外层的调用函数,在cpu计算和内存会消耗很多资源,当深度过大时会出现堆栈溢出

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=0) {
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)这样能够有效防止堆栈溢出。在ECMAScript6新加入尾递归优化,通过尾递归优化javascript代码在解释成机器码执行的时候会向while看齐,也就是说能够拥有数学表达能力和while的效能。

尾递归问题

尾递归的判断标准是函数运行(最后一步)是否调用自身,而不是是否在函数的最后一样调用自身,最后一行调用其他的函数或者表达式并返回叫尾调用,按道理尾递归调用调用栈永远都是更新当前的栈帧而已,这样就完全避免了爆栈的危险,但是现有的浏览器并未完全支持原因有二①在引擎层面消除递归是一个隐式的行为,程序员意识不到②堆栈信息丢失了开发者难以调试。浏览器不支持可以把递归改成while。

function runStack(n){
if(n === 0) return 100;

return runStack(n-2);
}
//会引起爆栈
runStack(50000)

//使用while改写
function runStack(n){
while(true){
if(n === 0){
return 100;
}
if(n === 1){
//防止陷入死循环
return 200;
}

n=n-2;
}
}


使用蹦床函数避免递归

//返回自身的另外一个版本
function runStack(n){
if(n === 0) return 100;
return runStack.bind(null, n-2);
}


function trampoline(f){
while(f&&f instanceof Function){
f = f();
}

return f;
}

trampoline(runStack(1000000))

蹦床函数的实际使用案例

function trampoline(fun) {
let result = fun.apply(fun, _.reset(arguments))
while(_.isFunction(result)){
result = result();
}

return result;
}

function partial1(fun, arg1){
return fun.bind(undefined, arg1)
}

function isEvenSafe(n){
if(n === 0){
return true;
}else {
return trampoline(partial1(oldOline, Math.abs(n) - 1))
}
}

function isOddSage(n){
if(n === 0){
return false;
} else {
trampoline(partial1(evenOline, Math.abs(n) - 1))
}
}

闭包

先来一段代码

function makePowerFn(power){
function powerFn(base){
return Math.pow(base, power);
}
}

const square = makePowerFn(2);
square(3)//9

如上代码外层的makePowerFn函数执行完毕,栈上的调用帧被释放,但是堆上的作用域并不会被释放,因此power依旧可以被powerFn函数访问形成闭包。

范畴与容器

可以把范畴想象成是一个容器,里面包含两样东西值value ,值的变形关系也就是函数,范畴论就是使用函数表达范畴之间的关系。伴随着范畴论的发展,出现了一整套函数的运算方法,这套方法起初值用于数学运算,后来有人将它再计算机上实现了,就变成了今天的函数式编程。本质上函数式编程知识范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,知识碰巧它能用来写程序。为什么函数式编程要求函数必须是纯的,不能又副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。
函数不仅可以用于将一个愁烦之中的值转换,还可以用于将范畴转成另一个范畴,这里就涉及到了函子(functor)。函子是函数式变成里面最重要的数据类型,也是基本的运算单位和功能单位,它首先是一种范畴,也就是说是一个容器,包含了值和变形关系,比较特殊的是它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。

Locale Dropdown

Functor(函子)是一些遵守特定规则的容器类型,Functor是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力,把东西装进一个容器,只留出一个接口map给容器外的函数,map一个函数时我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,以至于拥有惰性求值、错误处理、异步调用等高级特性。

函子的代码实现

任何具有map方法的数据结构都可以当做函子的实现

const Container = function(x){
this.__value = x;
}
//函数式编程一般约定,函子有一个of方法
Container.of = x => new Container(x);
//一般约定,函子的标志就是容器具有map方法,该方法将容器里面的每一个值映射到另外一个容器
Container.prototype.map = function(f){
return Container.of(f(this.__value));
}

Container.of(3)
.map(x => x+1)
.map(x => 'Result if '+ x);

使用类实现函子

class Functor(){
constructor(val){
this.val = val;
}

map(f){
return new Functor(f(this.val))
}
}

(new Functor(2).map(function(two){
return two +2;
}))

如上函数Functor是一个函子,它的map方法接收函数f作为变形关系,返回一个新的函子,里面包含了被f处理过的数据。一般约定,函子的标志就是容器具有map方法,该方法将容器里的每一个值映射到另一个容器。函数式编程里面的运算都是通过函子完成,即运算不直接针对值,而是针对这个值的容器--函子。函子本身具有对外的接口map方法,各种函数就是运算符,通过接口接入容器引发容器里面值的变形。因此学习函数式编程实际就是学习函子的各种运算,由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种函子就有多少中函子。函数式编程就变成了运用不同的函子解决实际问题。

Pointed函子

函子只是实现了map契约的接口,Pointed函子是函子的子集。生成新的函子的时候使用了new命令,和函数式编程不符,因为new命令是面向对象编程的标志,函数式编程一般约定函子有一个of方法用来生成新的容器。

Functor.of = function(val){
return new Functor(val)
}
//数组包含of方法,是Pointed函子
Maybe函子

Maybe用于处理错误和异常,函子接受各种函数,处理容器内部的值。这里存在一个问题容器内部的值可能是一个空值比如null而外部函数未必有处理空值的机制,如果传入空值很有可能会出错

Functor.of(null).map(function(s){
return s.toUpperCase();
})
//会报类型错误
class Maybe extends Functor{
map(f){
return this.val ? Maybe.of(f(this.val) : Maybeof(null))
}
}

Maybe.of(null).map(function(s){
return s.toUpperCase()
})
//null

Maybe的函数实现方式

const Maybe = function(x){
this.__value = x;
}

Maybe.of = function(x){
return new Maybe(x);
}

Maybe.prototype.map = function(f){
return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
}

Maybe.prototype.isNothing = function(){
return (this.__value === null || this.__value === undefined)
}
Either函子

函数中try/catch/throw不是“纯”的,因为它们外部接管了函数,并且出错时会抛弃函数的返回值。Promise可以catch来集中处理错误、事实上Either并不只是用来做错误处理的,它代表了逻辑或,范畴学里的coproduc。
条件运算if/else是编程里最常见的运算之一,函数式编程里面使用Either函子表达,Either函子内部有两个值:左值(left)和右值(right),右值是正常情况下使用的值,左值是右值不存在时使用的默认值。

class Either extends Functor{
constructor(left, right) {
this.left = left;
this.right = right;
}

map(f){
return this.right ? Either.of(this.left, f(this.right)) :
Either.of(f(this.left), this.right)
}
}

Either.of = function(left, right){
return new Eigher(left, right)
}

//代替try/catch
const addOne = function(x){
reuturn x+1;
}

Either.of(5,6).map(addOne);//Either(5,7)
Eitherof(1, null).map(addOne);//2 null
Either.of({address: 'xxxx'}, currentUser.address).map(updateField);

使用Either函子实现错误处理

const left = function(x){
this.___value = x;
}

const Right = function(x){
this.___value = x;
}

Left.of = function(x){
return new Left(x);
}

Right.of = function(x){
return new Right(x);
}

Left.prototype.map = function(f){
return this;
}

Right.prototype.map = f unction(f){
return Roght.of(f(this.__value))
}

如上Left和Right的唯一区别就在于map方法的实现,Right.map的行为和之前的map函数一样,但是Left.map就有一些差异,它不会对容器做任何操作,只是取出容器的值然后传递回去,这种可以使用Left来传递一个错误消息。

错误处理Either案例

const getAge = user => user.age ? Right.of(user.age) : Left.of("Error");

getAge({name: 'stark', age: 21}).map(age => 'Age is ' + age)//Right('Age is 21')
getAge({name: 'stacl'}).map(age => 'Age is ' + age);//Left('ERROR')
AP函子

函子里面包含的值完全可能是函数,存在一个函子的值是数值另一个函子的值是函数的情况

class Ap extends Functor{
ap(f){
return Ap.of(this.val(f.val))
}
}

Ap.of(addTow).ap(Functor.of(2))
IO函子

真正的程序总要接触到一些副作用如:

function readLocalStorage(){
return window.localStorage;
}

IO函子和其他函子不同的地方在于它的__value是一个函数,它是不纯的操作(比如IO、网络请求、DOM操作)包裹到一个函数内,从而延迟这个操作的执行,所以IO包含的是副作用操作的返回值。IO函子其实也算一种惰性求值。

import _ from 'lodash';
const compose = _.flowRight;

<!-- const IO = function(f){
this.__value = f;
}

IO.of = x => new IO(_ => x);

IO.prototype.map = function(f){
return new IO(compose(f, this.__value))
} -->

class IO extends Monad{
map(f){
return IO.of(compose(f, this.__value))
}
}
//使用IO函子
const fs = require('fs');
const readFile = function(filename){
return new IO(function(){
return fs.readFileSync(fulename, 'utf-8')
})
}

readFile('./user.txt')
.chain(tail)
.chain(print0)

补充

Maybe、Either、IO三种强大的函子,在链式调用、惰性求值、错误捕获、输入输出中都发挥这巨大的作用,事实上Functor远不止这三种。

如何处理嵌套的Functor?(比如Maybe(IO(42))) 如何处理一个由非纯的或者异步的操作序列呢?

Locale Dropdown

数据类型就是对值的封装,不仅包含值本身,还包括相关的属性和方法,2就不是单纯的值而是一种数据类型的实例,只能在数据类型的场景(context)中使用。

Locale Dropdown

Locale Dropdown

Monad

Monad就是一种设计模式,表示将一个运算过程通过函数拆成互相连接的多个步骤,只要提供下一步运算所需的函数,整个运算就会自动进行下去。Promise就是一种Monad,Modad让我们避开了嵌套地狱,可以轻松地进行深度嵌套的函数编程,比如IO和其他异步任务

Maybe.of(
Maybe.of(
Maybe.of({name: 'Mulburry', number: 4321})
)
)

class Monad extends Functor{
join(){
return this.val;
}

flatMap(f){
return this.map(f).join();
}
}

Monad函子的作用是,总是返回一个单层的函子,它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个返程的容器,不会出现嵌套的情况,如果函数f返回的是一个函子,那么this.map(f)就会生成一个嵌套的函子,所以join方法保证了flatMap总是返回一个单层的函子,这意味着嵌套的函子会被拉平(flatten)。

当 下函数式编程最热的库

  • RxJS
  • cycleJS
  • lodashJS、lazy(惰性求值)
  • underscoreJS
  • ramdajs

RXJS

Rxjs从诞生以来就一直不温不火,但它是函数响应式编程(Functional Reactive Programming, FRP)的理念非常先进,虽然获取对于大部分应用环境来说,外部输入事件并不是太频繁,并不需要引入一个如此庞大的FRP体系,但可以了解一些它的优秀特性。在RxJS中所有的外部输入(用户输入、网络请求等)都会被视作一种事件流
例子:没2次点击事件产生一次事件响应

const clicks = Rx.Observable.
.formEvent(document, 'click')
.bufferCount(2)
.subscribe(x => console.log(x))

响应式编程是继承自函数式编程,声明式的、不可变的、没有副作用是函数式编程的三大护法。一直使用面向对象范式编程的我们习惯了用变量存储和追踪程序的状态。RxJS从函数式编程范式中借鉴了很多东西比如链式函数调用、惰性求值等。在函数中与函数作用域之外的一切事物有交互的就产生了副作用,比如读写文件、在控制台打印语句、修改页面的css等。在RXjs中把副作用问题推给了订阅者来解决。

CycleJS

Cycle.js是一个基于Rxjs的框架,它是一个彻彻底底的FRP理念的框架和React一样支持virtual DOM、JSX语法,目前大型应用使用的少。本质上讲它就是在RXJS的基础上加入了对virtualDOM、容器和组件的支持

function main(success){
const sinks = {
DOM: sources.DOM.select('input').events('click')
.map(ev => ev.target.checked)
.startWidth(false)
.map(toggled =>
<div>
<input type="checkbox"/> toggle me
<p>{toggled ? 'ON' : 'off'}</p>
</div>
)
}

return sinks;
}

const drivers = {
DOM:makeDOmDriver('#app')
}

run(main, drivers);

Underscore.js

Underscore是一个javascript工具库,它提供了一整套函数式编程的实用功能,但是没有扩展任何javascript内置对象。UnderScore提供了100多个函数,包括常用的map、filter、invoke,当然还有更多专业的辅助函数,入:函数绑定、Javascript模板功能、创建快速索引、强类型相等测试等等。

Lodash.js

lodash是一个具有一致接口、模块化、高性能等特性的JavaScript工具库,fork自underscore,其最初的目标也是“一致的跨浏览器行为”,并改善其性能。lodash采用延迟计算,意味着链式方法在显式或者隐式的value()调用之前不会执行的,因此lodash可以进行shortcut(捷径)fusion(融合)这样的优化,通过合并链式大大降低迭代的次数,从而大大提升其执行性能

Ramdajs

ramda是一个非常优秀的js工具库,跟同类比跟函数式体现在以下几个原则

  1. Ramda的数据一律放在最后的一个参数,理念是function first,data last。R.map(square, [3,2])
  2. ramda里面提供的函数全部都是curry的也就是说所有多参数的函数,默认都可以单参数使用
  3. Ramda推崇pointfree简单的说就是使用简单函数组合实现一个复杂的功能,而不是单独写一个函数操作临时变量
  4. ramda有个非常好用的参数占位符R._大大减轻了函数在pointfree过程中参数位置的问题
  5. 相比underscore/lodash感觉赶紧很多

函数式编程的实际应用场景

易调试

函数式编程中的每个符号都是const的,于是没有什么函数会有副作用,谁也不能再运行时修改任何东西,也没有函数可以修改在它的作用域之外修改什么值给其他函数继续使用,这意味着决定函数执行结果的唯一因素就是它的返回子,而影响其返回值的唯一因素就是它的参数。

并发

函数式编程不需要考虑死锁,因为它不修改变量,所以根本不存在锁线程的问题,不必担心一个线程的数据被另一个线程修改所以可以很放心的把工作分摊到多个线程,部署并发编程

易部署

函数式编程中所有的状态就是传给函数的参数,而参数都是存储在栈上的,这一特征让软件的热部署变得十分简单,只要比较一下正在运行的代码以及新的代码获得diff然后用这个diff更新现在有的代码,新代码的热部署就完成了

单元测试

严格函数式编程的每一个符号都是对直接量或者表达式结果的引用,没有函数产生副作用。因为从未在某个地方修改过值,也没有函数修改过其作用域之外的量并被其他函数使用(如类成员或全局变量),这意味着函数求值的结果只是其返回值,而唯一影响其返回值的就是函数的参数。这很易于单元测试,对被测试的程序中的每个函数,只需要在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部状态。所有要做的就是传递代表了边际情况的参数。如果程序中的每个函数都通过了单元测试,对软件的质量也有了相当的自信,而命令式编程就不能这样乐观了。

总结

函数式编程不能视为灵丹妙药,它是我们现有工具箱的一个补充--带来了更高的可组合性、灵活性及容错性。现代JavaScript库已经开始尝试拥抱函数式编程的概念以获取这些优势,Redux作为一种Flux的变种实现,核心理念也是状态机和函数式编程