函数式编程简介-附入门方法

WHAT? 什么是函数式编程?

函数式编程是一种编程范式。

编程范式又是什么?
编程范式是一种解决问题的思路。
我们熟悉的命令式编程把程序看作一系列改变状态的指令;而函数式编程把程序看作一系列数学函数映射的组合
编程范式和编程语言无关,任何编程语言都可以按照函数式的思维来组织代码。

i++; // 命令式 关心指令步骤
[i].map(x => x + 1); // 函数式 关心映射关系

WHY? 函数式有什么好处?

  • 易写易读 聚焦重要逻辑,摆脱例如循环之类的底层工作
  • 易复用 面向对象可复用的单位是类,函数式可复用的是函数,更小更灵活
  • 易测 纯函数【后面会讲】不依赖外部环境,测试起来准备工作少
  • 看起来很厉害 被人夸奖能增强信心和动力,所以这点也很重要

HOW? 如何做起?

方法不难,回学校念个博士,搞清楚范畴论,幺半群之类的就可以了。

人生苦短,还是来点实际的吧。

  1. filter map reduce 三板斧用好,从循环中解放出来
  2. small pure function 多写小的纯函数,小指功能聚焦
  3. compose pipeline curry 三个工具利用好,把小函数像搭积木一样拼成大函数

filter map reduce 三板斧

来个例子:找出集合中的素数,算出它们平方的和。

独孤九剑之命令式

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循环没了,代码意图也更明显了。

  1. filter(isPrimeNumber) 找出素数
  2. map(x => x * x) 变成平方
  3. 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

纯函数有两点要求:

  1. 相同的传参,返回值一定相同
  2. 函数调用不会对外界造成影响,如不会修改外部对象

看个例子

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内的函数是从右往左运行的,也就是先holifyexclaimupperCase

有人可能看不惯从右往左运行,于是又有了一个pipeline

pipeline

compose的区别就是换个方向,compose用的是reduceRightpipeline用的是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);

总结

函数式是一种编程思维,声明式、更抽象。

这种思维方式的利弊,大型项目里怎么用,我还没深刻的体会,练习还不足。

建议新手和我一样从下面三点开始多写多思考。

  1. filter map reduce 三板斧用好,从循环中解放出来
  2. small pure function 多写小的纯函数,小指功能聚焦
  3. compose pipeline curry 三个工具利用好,把小函数像搭积木一样拼成大函数

后面我会继续学习functor monad相关的知识,感兴趣可以关注。

猜你喜欢

转载自www.cnblogs.com/apolis/p/9370847.html