Day 8 - Functional Programming 初探 (1) - HoF 与 Side Effects

前言

Functional Programming 其实是我相对不熟的主题,但因为在写一些较难的程序时,往往会突然感受到有这些神奇的力量存在(?),再加上现在 forEachmapfilter 这麽普及,总觉得趁这个机会好好整理一下。

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 那麽简单:

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

如果我今天需要筛选的是

  • 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,这是抽象化非常厉害的地方呢!

如果我说明得不够清楚,也欢迎大家补充,或者可以看看 Quora 的网友怎麽看

Pure Functions(纯函式)

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

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

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

白话一点:

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

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

Side Effects

side effects 我们有在 Day 6 - Function 时空旅行 (1) 提到过,如字面上的意思就是副作用,翻成白话应该是:「你做的事影响到其它人」。

常见的 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

结语

今天介绍了关於 FP 几个常见的特性,尤其是关於 HoF 的意义,我自己也在查资料的过程中思考了许多,有一些思维其实没有一定的做法,但总是会在碰到某些困难时,灵光一闪觉得「好像可以这样用!」,我想这就是学习不同 coding 思维很有趣的地方!

幻化
在空旷的荒野洒落
通向八方的道路

参考资料

hannahpun - Function Programming In JS
Po-Ching Liu - javascript-functional-programming
Why-are-higher-order-functions-important-to-functional-programming


<<:  [Day 12] 第一主餐 pt.5-MySQL Django一起串联,就是这麽简单

>>:  D8 - 彭彭的课程#Python 集合、字典的基本运算 - Set、Dictionary

LINE JP 电脑版 个别视窗 聊天室 变成 半透明

LINE JP 电脑版 个别视窗 聊天室 变成 半透明 如何改回来 拉动上面那个 就回来了 #3小姐...

selenium爬虫:使用xpath

from selenium import webdriver import openpyxl imp...

Day18 订单 -- 优惠项目

前几天我们把购物车流程跑完了,其中有讲到优惠的部份, 因此我们订单需要新增table来储存该内容,这...

Day 16. slate × Interfaces × CustomType

slate 将 typescript 的型别扩充相关的内容都集合在 interfaces/cust...

[2020铁人赛Day30]糊里糊涂Python就上手-体验 OpenCV 人脸辨识

今日目标 轻松小品,来点 OpenCV 人脸辨识的实作 What is OpenCV? OpenCV...