函数式编程是一种编程范式。
编程范式又是什么?
编程范式是一种解决问题的思路。
我们熟悉的命令式编程把程序看作一系列改变状态的指令
;而函数式编程把程序看作一系列数学函数映射的组合
。
编程范式和编程语言无关,任何编程语言都可以按照函数式的思维来组织代码。
i++; // 命令式 关心指令步骤 [i].map(x => x + 1); // 函数式 关心映射关系
易写易读
聚焦重要逻辑,摆脱例如循环之类的底层工作易复用
面向对象可复用的单位是类,函数式可复用的是函数,更小更灵活易测
纯函数【后面会讲】不依赖外部环境,测试起来准备工作少看起来很厉害
被人夸奖能增强信心和动力,所以这点也很重要
方法不难,回学校念个博士,搞清楚范畴论,幺半群之类的就可以了。
人生苦短,还是来点实际的吧。
filter
map
reduce
三板斧用好,从循环中解放出来small pure function
多写小的纯函数,小指功能聚焦compose
pipeline
curry
三个工具利用好,把小函数像搭积木一样拼成大函数
来个例子:找出集合中的素数,算出它们平方的和。
const isPrimeNumber = x => { if (x <= 1) return false; let testRangStart = 2, testRangeEnd = Math.floor(Math.sqrt(x)); let i = testRangStart; while (i <= testRangeEnd) { if (x % i == 0) return false; i++; } return true; }; const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; let sum = 0; for (let i = 0; i < arr.length; i++) { if (isPrimeNumber(arr[i])) { sum += arr[i] * arr[i]; } } console.log(sum);
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const sum = arr.filter(isPrimeNumber) .map(x => x * x) .reduce((acc, cur) => acc + cur, 0); console.log(sum);
看吧,for
循环没了,代码意图也更明显了。
filter(isPrimeNumber)
找出素数map(x => x * x)
变成平方reduce((acc, cur) => acc + cur, 0)
求和
是不是比命令式看着更清晰了?
isPrimeNumber
的函数式写法也放出,去掉了循环,看看好懂不。
// 输入范围,获得一个数组,例如 输入 1和5,返回 [1, 2, 3, 4, 5] const range = (start, end) => start <= end ? [start].concat(range(start + 1, end)) : []; const isPrimeNumber = x => x >= 2 ? range(2, Math.floor(Math.sqrt(x))).every(cur => x % cur != 0) : false;
有人说函数式的效率不高,因为filter
map
reduce
每次调用,内部都会遍历一遍集合,而命令式只遍历了一次。
函数式是更高级的抽象,主要声明解决问题的步骤,把性能优化交给框架或者runtime来解决。
- 框架
transducer
可以让集合只遍历一次【篇幅有限,这里不展开】memorize
记录已经算过的,提高效率【后面讲纯函数的时候,会给出实现】 - runtime
有的语言map
是多线程运行的,函数式代码不变,runtime一优化,性能就大幅的提升了,而前面的命令式,就做不到这一点。
small pure function
纯函数有两点要求:
- 相同的传参,返回值一定相同
- 函数调用不会对外界造成影响,如不会修改外部对象
看个例子
let name = 'apolis'; const greet = () => console.log('Hello ' + name); greet(); name = 'kzhang'; greet();
greet
函数依赖外部变量name,相同的传参【都不传参也算相同的传参】屏幕输出的内容却不一样,所以它不纯,鉴定完毕。
const greet = name => console.log('Hello ' + name);
这样就好多了,不受外部变量的影响了。
不过更严格的认为,调用这个函数造影响了控制台console,所以还不算纯。
const greet = name => 'Hello ' + name;
这样才够纯,同时greet
也摆脱了对控制台的依赖,可以适用的范围更广了。
我们要学会把纯的留给自己,把不纯的甩给别人......咳咳,关在函数外面。
由于它的纯,同样的传参,返回值一定相同。
我们可以把算过的结果保存下来,下次调用传的参数发现算过了,直接返回之前计算的结果,提升效率。
const memorize = fn => { let cache = {}; return x => { if (cache.hasOwnProperty(x)) return cache[x]; else { const result = fn(x); cache[x] = result; return result; } } };
利用上面的工具函数,我们可以缓存纯函数的计算结果,三板斧的例子filter
改一下就可以了。
const sum = arr.filter(memorize(isPrimeNumber)) .map(x => x * x) .reduce((acc, cur) => acc + cur, 0); console.log(sum);
如果数组中包含重复元素,这样就能减少计算次数了。
命令式写法要达到这个效果,改动就大的多了。
compose pipeline curry
写了一堆small pure function
,怎么把他们组合成更强大的功能呢?
compose
pipeline
curry
这三位该出场了。
compose
举个例子。
const upperCase = str => str.toUpperCase(); const exclaim = str => str + '!'; const holify = str => 'Holy ' + str;
现在需要一个amaze
方法,字符串前面添加Holy,后面添加叹号,全部转为大写。
const amaze = str => upperCase(exclaim(holify(str)));
很不优雅对不对?
看看compose
怎么帮我们解决这个问题。
const compose = (...fns) => x => fns.reduceRight((acc, cur) => cur(acc), x); const amaze = compose(upperCase, exclaim, holify) console.log(amaze('functional programing'));
这里用到了reduceRight
,和reduce
的区别就是数组是从后往前遍历的。compose
内的函数是从右往左运行的,也就是先holify
再exclaim
再upperCase
。
有人可能看不惯从右往左运行,于是又有了一个pipeline
。
pipeline
和compose
的区别就是换个方向,compose
用的是reduceRight
,pipeline
用的是reduce
。
const pipeline = (...fns) => x => fns.reduce((acc, cur) => cur(acc), x); const amaze = pipeline(holify, exclaim, upperCase) console.log(amaze('functional programing'));
curry
上面compose
pipeline
里的函数参数都只是一个,如果函数要传多个参数怎么办?
解决办法就是用curry
【柯里化】,把函数变成一个参数的。
const add = (x, y) => x + y; const multiply = (x, y) => x * y;
这两个函数都是需要传两个参数的,现在我需要一个函数,把数字先加5再乘2。
const add5ThenMultiplyBy2 = x => multiply(add(x, 5), 2)
很不好看,我们来curry
一下再compose
看看。
怎么curry
?
把括号去掉,逗号变箭头就可以了。
这样传入一个参数x
的时候,返回了一个新函数,等待着接收参数y
。
const add = x => y => x + y; const multiply = x => y => x * y;
接下来,我们又可以用compose
了
const add5ThenMultiplyBy2 = x => compose(multiply(2), add(5));
不过curry
之后的add
方法要这么调用了
add(2)(3)
原先的调用方式add(2, 3)
都得改掉了。不喜欢这个副作用?再奉上一个工具函数curry
。
const curry = fn => { const inner = (...args) => { if (args.length >= fn.length) return fn(...args); else return (...newArgs) => inner(...args, ...newArgs); } return inner; };
传入fn
返回一个新函数,新函数调用时判断传入的参数个数有没有达到fn
的要求,达到了,直接返回fn
调用的结果;没达到,继续返回一个新新函数,记录着之前已传入的参数。
const add = (x, y) => x + y; const curriedAdd = curry(add);
这样两种调用方式都支持了。
curriedAdd(2)(3); curriedAdd(2, 3);
函数式是一种编程思维,声明式、更抽象。
这种思维方式的利弊,大型项目里怎么用,我还没深刻的体会,练习还不足。
建议新手和我一样从下面三点开始多写多思考。
filter
map
reduce
三板斧用好,从循环中解放出来small pure function
多写小的纯函数,小指功能聚焦compose
pipeline
curry
三个工具利用好,把小函数像搭积木一样拼成大函数
后面我会继续学习functor
monad
相关的知识,感兴趣可以关注。