彻底搞懂Functional Programming(一)

2,415 阅读5分钟

Functional Programming 思维

Functional Programming (简称 FP),是一种撰写风格,我觉得更像是一种抽象化的思维,因为用这种思维下去写的 code,会以function为操作的主体。

我觉得 FP 有点像是生产线的思维,第一个员工专门负责备料,第二个员工会接着用这些材料制作成料理,第三个员工就把这些料理送到客人手上。

而每一个员工都像是一个 function,它们不管其它的,只管自己手上的任务,它们的台词可能是像这样:

  • 「我就只会切菜,我就专门切菜,切菜以外的我都不管」
  • 「只要上一个员工有给我菜,我一定会把它切好传给下一个员工」
  • 「我不会去影响其他员工,连聊天都不会」

让大家对 FP 有个超级基础的概念,但今天还不会真的写到 code,因为我们要先来了解很多 FP 的名词跟观念:

First-class (一等公民)

要写 FP 的首要条件就是,function 必须是这个语言的一等公民,代表跟其他资料型别具有同等地位,也就是要拥有这些特性:

  • function 可以用来赋值给变数
const addNum = (x, y) => x + y; 
  • function 能被当作参数传入
arr.map(num => num + 1);
  • function 能被当作回传值
const addNum = (x) => {
  return (y) => {
    return x + y;
  };
};

这是硬条件哦!少了这个就不能叫 FP 了。

Higher-order Functions (高阶函式)

以下两者符合一项,即是高阶函式 (简称 HoF):

  • 可以将函式当成参数传入另一个函式
  • 可以将函式当成另一个函式的回传值

虽然我们一般写 function,会拿来当参数的,大部分都是 string、array 或 object 之类的。

但仔细一想却会发现:

arr.forEach(() => {});
arr.map(() => {});
arr.filter(() => {});
arr.reduce(() => {}, initialValue);

啊啊啊原来到处都在拿 function 当参数啊!虽然也没有很普遍,但起码不算陌生。不过。。。

HoF 带来了什么好处?

比起 HoF 「是什么」,网路上反而很少在讨论「为什么」要有 HoF?它给 FP 带来了什么好处? 其实我还真不知道,于是我也乖乖去 google 了一下:

functional programming higher order functions "why"

没错我还特地把 why 用双引号框起来,才比较找得到

得到一个最重要的结论是,HoF 让程式可以比较容易「抽象化」。

我试着讲讲我的理解,这边很期待能有朋友一起补充。

比较 HoF 的 before & after

比如我们熟悉的 filter 就是 HoF,那如果在没有 HoF、没有 filter 的情况下,我们要怎么做到「筛选」这件事呢:

// 筛选出 10 以下的数字
const arr = [3, 6, 9, 12, 15];
const lessThanTen = [];
for (let i=0; i<arr.length; i++) {
    if (arr[i] < 10) {
        lessThanTen.push(arr[i]);
    }
}
console.log(lessThanTen);
执行结果

[3, 6, 9]
那现在使用 HoF 来做,会变成:

// 筛选出 10 以下的数字
const arr = [3, 6, 9, 12, 15];
const lessThanTen = arr.filter(num => num < 10);
console.log(lessThanTen);
执行结果

[3, 6, 9]

OK,先不要把重点放在 filter 的 code 比较少这件事,因为如果把 filter 底层的 code 翻出来,执行的量绝对不会少于上面的 for 回圈。

重点在于,我们抽象化了「筛选」这个动作。

我们抽象化了「筛选」这个动作。

抽象化了「筛选」

系统提示:你看到脑中的回音了

抽象化的意义

抽象化并不是把 for 回圈拉出去当 function 那么简单: 如果我今天需要筛选的是

  • 5 以下的数字
  • 乘以 3 是 2 的倍数的数字
  • 与今天月份相同的数字

我是不是还要为了这几个特别的 case,又多写三个 function 出来?

听起来就很难维护啊!所以我想抽象化就是为了解决这个问题,可能称作「客制化」的问题吧!

我们把筛选这个动作抽象化,所有想要做「筛选」动作的,都可以呼叫filter,然后根据你的需求,把判断用的 function 丢进参数,就完成一个「客制」的 filter 了。

const arr = [3, 6, 9, 12, 15];

// 筛选出 5 以下的数字
arr.filter(num => num < 5);
// 乘以 3 是 2 的倍数的数字
arr.filter(num => (num * 3) % 2 === 0);
// 与今天月份相同的数字
arr.filter(num => num === new Date().getMonth() + 1);
执行结果

[3]
[6, 12]
[9]

所以或许可以这样说,HoF 能够赋予 function 在某个基础上客制化的能力

回传函式的 HoF

比如我们自己来写一个,回传 function 的 HoF:

const addNum = (x) => {
  return (y) => {
    return x + y;
  };
};
// 可简化成
// const addNum = x => y => x + y;

const addFive = addNum(5);
const addTen = addNum(10);

addFive(3); // 8
addTen(3); // 13

有感受到了吗?透过 addNum 这个 HoF,我们可以很快「客制」出两个额外的 function,分别处理 + 5 与 + 10 的 case,这是抽象化非常厉害的地方呢!

Pure Functions (纯函式)

关于纯函式的定义,在维基可以看到比较精准的定义:

函式与外界交换资料只有一个唯一渠道—— 参数和回传值

  • 函式从函式外部接受的所有输入资讯,都通过参数传递到该函式内部
  • 函式输出到函式外部的所有资讯,都通过回传值传递到该函式外部

白话一点:

在函式内出现的变数,要嘛是函数内自己宣告的,要嘛是从参数传进来的,有其他来源的话就是 impure

而 impure 的函式,就代表函式里面有 side effects。

Side Effects

side effects 我们有在彻底掌握 Function (一)提到过,如字面上的意思就是副作用,翻成白话应该是:「你做的事影响到其它人」。

常见的 side effects 如下:

  • 发送 http request (如fetchaxios)
  • 在画面印出值或是 log (如console.log)
  • 操作 DOM 物件 (如docuement.querySelector)

我想许多人会感到困惑的应该是这个吧。。。

console.log怎么也算 side effects 啊!它招谁惹谁了 QQ 把东西印出来又不会出事!

这部分算是我也还在理解的,我想是因为 pure function 要的是完全的纯粹,也就是这个 function 里面只要做好它「该做的事」。

而像 console.log 这样其实是去呼叫 window.console.log 的指令,一来它「不是该做的事」,二来它就是「影响到别人了」。

100% 的纯度?

这边需要强调一点,不用强硬追求 100% 的 pure,或者 100% 没有 side effects,因为如果真的达到 100% 了,是不是也不能够发送 http request 跟操作 DOM 了呢?

我认为要追求的是,尽可能让有 side effects 的程式码被集中 (共用),不要东一个西一个,才能够将测试时的负担降到最低。

Pure 追求的不是

zero side effects

而是

minimize side effects

结语