通用的函数式编程语言,是Haskell,被函数式原教旨主义者认为是纯函数式语言。函数式编程的思想也不断影响着传统编程语言,比如Java 8开始支持lambda表达式,而函数式编程的大厦最初就是基于lambda计算构建起来的。函数式编程对于前端来说是必选项,后端则不必。
React框架的组件从很早开始就是不仅支持类式组件,也支持函数式的组件。而后React Hooks的出现,使得函数式编程思想越来越变得不可或缺。
这是函数式编程的精髓所在。
无副作用的函数应该符合下面的特点:
要有输入参数。如果没有输入参数,这个函数拿不到任意外部信息,也就不用运行了。
要有返回值。如果有输入没有返回值,又没有副作用,那么这个函数白调了。
对于确定的输入,有确定的输出
数学函数就是如此的:
const sqr3 = function(x){
return x * x * x;
}
console.log(sqr3(2));
无副作用函数拥有三个巨大的好处:
可以进行缓存。我们就可以采用动态规划的方法保存中间值,用来代替实际函数的执行结果,大大提升效率。
可以进行高并发。因为不依赖于环境,可以调度到另一个线程、worker甚至其它机器上,反正也没有环境依赖。
容易测试,容易证明正确性。不容易产生偶现问题,也跟环境无关,非常利于测试。
在会无副作用的函数之后,需要好的将这些函数组合起来。
上述的 sqr3 函数有个问题,如果不是number类型,计算就会出错。按照命令式的思路,我们可能就直接去修改sqr2的代码,比如改成这样:
/** 命令式 */
const sqr3 = function(x){
if (typeof x === 'number'){
return x * x * x;
}
return 0;
}
/** 函数式 */
const isNum = x => typeof x === 'number';
console.log(sqr3(isNum("20")));
或者是我们在设计sqr3的时候就先预留出来一个预处理函数的位置,将来要升级就换这个预处理函数,主体逻辑不变:
/** fn 作为 预处理函数 */
const sqr3 = function(fn, x){
const y = fn(x);
return y * y;
}
const sqr3New = function(x){
return sqr3(isNum,x);
}
console.log((sqr3New(2.2)));
如果我们想给其他的函数也复用这个isNum的能力,可以封装一个容器对象来提供这个能力:
class MayBeNumber{
constructor(x){
this.x = x;
}
map(fn){
if (isNum(this.x)){
return MayBeNumber.of(fn(this.x));
}
return MayBeNumber.of(0);
}
getValue(){
return this.x;
}
}
这样,我们不管拿到一个什么对象,用其构造一个MayBeNumber对象出来,再调用这个对象的map方法去调用数学函数,就自带了isNum的能力:
const notnum = new MayBeNumber(undefined).map(Math.sin).getValue();
console.log(notnum);
可以发现,输出值从NaN变成了0。而且封装到对象中的另一个好处是可以用"."多次调用了。
const num = new MayBeNumber(1).map(Math.sin).map(sqr2).getValue();
console.log(num);
上面的封装到对象中,但是函数式编程还搞出来new对象再map,最好是构造对象也是个函数。给它定义个 of 方法:
MayBeNumber.of = function(x){
return new MayBeNumber(x);
}
const num = MayBeNumber.of(2).map(Math.tan).map(Math.exp).getValue();
console.log(num);
再来看下另一种情况,我们处理返回值的时候,如果有Error,就不处理Ok的返回值,可以这么写:
class Result{
constructor(Ok, Err){
this.Ok = Ok;
this.Err = Err;
}
isOk(){
return this.Err === null || this.Err === undefined;
}
map(fn){
return this.isOk()
? Result.of(fn(this.Ok),this.Err)
: Result.of(this.Ok, fn(this.Err));
}
}
Result.of = function(Ok, Err){
return new Result(Ok, Err);
}
console.log(Result.of(2, undefined).map(sqr3));
// 输出结果为:Result { Ok: 8, Err: undefined }
这是一种容器的设计模式:
有一个用于存储值的容器
这个容器提供一个map函数,作用是map函数使其调用的函数可以跟容器中的值进行计算,最终返回的还是容器的对象
我们可以把这个设计模式叫做Functor函子。如果这个容器还提供一个of函数将值转换成容器,那么它叫做Pointed Functor。比如 JavaScript 中的Array类型。
借助Result结构,对sqr3的返回值进行格式化。如果是数值的话,Ok是数值,Err是undefined。如果非数值的话,Ok是undefined,Err是0:
const sqr3Res = function(x){
if (isNum(x)){
return Result.of(x * x * x, undefined);
}
return Result.of(undefined, 0);
}
console.log(Result.of(4.3, undefined).map(sqr3Res));
// 输出结果:Result { Ok: Result { Ok: 18.49, Err: undefined }, Err: undefined }
返回的是一个嵌套的结果,但是我们需要的是子Result的值。需要个Result 加一个 join函数:
class Result{
constructor(Ok, Err){
this.Ok = Ok;
this.Err = Err;
}
isOk(){
return this.Err === null || this.Err === undefined;
}
map(fn){
return this.isOk()
? Result.of(fn(this.Ok),this.Err)
: Result.of(this.Ok, fn(this.Err));
}
join(){
if (this.isOk()) {
return this.Ok;
}
return this.Err;
}
flatMap(fn){
return this.map(fn).join();
}
}
Result.of = function(Ok, Err){
return new Result(Ok, Err);
}
console.log(Result.of(3, undefined).flatMap(sqr3Res));
// 输出结果:Result { Ok: 27, Err: undefined }
不严格地讲,像Result这种实现了flatMap功能的 Pointed Functor,就是传说中的Monad。
函数式编程与命令行编程体感上的最大区别:
函数是一等公式,我们应该熟悉变量中保存函数再对其进行调用
函数可以出现在返回值里,最重要的用法就是把输入是n(n>2)个参数的函数转换成n个1个参数的串联调用,这就是传说中的柯里化。这种减少了参数的新函数,我们称之为偏函数
函数可以用做函数的参数,这样的函数称为高阶函数。
如何用函数式方法实现一个只执行一次有效的函数?
once是一个高阶函数,返回值是一个函数,如果done是false,则将done设为true,然后执行fn。done是在返回函数的同一层,所以会被闭包记忆获取到:
const once = (fn) => {
let done = false;
return function() {
return done ? undefined : ((done=true), fn.apply(this,arguments));
}
}
const initData = once(
() => {
console.log("Initialize data");
}
);
initData();
initData();
可以发现,第二次调用init_data()没有发生任何事情。
递归是函数式编程中比较复杂的,最简单的递归就是阶乘:
const factorial = (n) => {
if (n === 0){
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(10));
如此会重复计算好多次,效率较低,应该使用动态规划或者缓存记忆。没错,我们可以封装一个叫memo的高阶函数来实现这个功能:
const memo = (fn) => {
const cache = {};
return (arg) => cache[arg] || (cache[arg] = fn(arg));
}
使用memo的后阶乘:
const fastFact = memo(
(n) => {
if (n <= 0){
return 1;
}
return n * fastFact(n-1);
}
);
言归前端,React Hooks 中的 useMemo就是使用的这种记忆机制:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
在日常使用中,需要记住这几点:
函数式编程的核心就是将函数存到变量里,用在参数里,用在返回值里;
在编程时要时刻记住将无副作用与有副作用代码分开;
函数式编程背后有其数学基础,不仅仅是一种设计模式。