欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-05-18 18:26:27
函数式编程是一种解决问题的思路。我们熟悉的命令式编程把程序看作”一系列改变状态的指令“;而函数式编程把程序看作”一系列数学函数映射的组合“。 ......

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相关的知识,感兴趣可以关注。