Day 9 - Functional Programming 初探 (2) - Currying 与 Composition

前言

今天会继续来聊聊 FP 的一些重要观念,而且会更偏向实际的做法,看看 Javascript 怎麽结合昨天聊到的 First-class、HoF、pure function,并且落实「以 function 为主体」来写程序。

Imperative vs. Declarative

新手们通常看到这两个单字就直接转台了,如果你成功看到第二行,我会尽量讲清楚来报答你(?)。

Imperative

中文翻成命令式,是一个胼手胝足、努力向上的好青年,他做事的每一个步骤你都看在眼里,他关注的是细节,是「如何做到」。

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];

let totalPrice = 0;
arr.forEach(item => {
    if(item.price > 10000) {
        totalPrice += item.price;
    }
});

Declarative

中文翻成宣告式,是主管的类型,在公司的重要会议里面,他只会告诉你大方向及一些策略,他关注的是整体,是「要做什麽」。

const arr = [
    { id: 'item1', name: 'TV', price: 13500 },
    { id: 'item2', name: 'washing machine', price: 8200 },
    { id: 'item3', name: 'laptop', price: 25000 },
];

const totalPrice = arr.filter(item => item.price > 10000)
                      .map(item => item.price)
                      .reduce((prev, curr) => prev + curr, 0);

两者差异

有没有觉得上面两个范例有点眼熟。。。没错就是我们在 Day 2 - Array 阵列组合技 (1) 讨论到的,关於 forEach v.s. 联合军(filter/map/reduce) 的写法。

当时提到了会有效能与可读性方面的 trade-off,但没有特别提到命令式与宣告式,因为我忘了我想让大家多感受一些阵列与物件的用法,等到现在正式来讨论 FP 的时候,才不会觉得越级打怪。

现在是不是觉得很多线索都串连起来了?

虽然 forEach 比起一般的 for 回圈来说,已经算是比较偏向宣告式了,当你看到 forEach 这个关键字的时候,你会立马知道:

  • forEach:我要跑回圈,而且会把阵列中每个元素跑过一遍

但比起联合军(filter/map/reduce)的成员来说,他们更宣告了一点,因为每个都更明确告诉你:

  • filter:我不只要跑回圈,还要做一个筛选的动作
  • map:我不只要跑回圈,还要做一个转换的动作
  • reduce:我不只要跑回圈,还要做一个整合的动作

谁好谁坏?

首先,没有好坏,虽然很多人比较想当主管,但那只是个拟人的形容,不要太入戏,两种都是程序写法的差异罢了,之前在 Day 2 也讨论过,有些时候选择不同的写法都只是 trade-off 而已,没有绝对正确的答案。

不过正是因为没有正确答案,才更要去理解,这两种的擅长点各自在哪,才能够在对的地方发挥出来。

屠龙宝刀虽然厉害,但对於猪肉贩来说,一般的铁菜刀反而更顺手。
如果我是猪肉贩,我会选屠龙宝刀,然後卖掉买很多把铁菜刀((拖走

Currying

篮球迷要疯狂了!咖哩控也要暴走了!怎麽学 FP 还有球可以打有咖哩可以吃,原来是来源於一位逻辑学家叫做 Haskell Curry

currying 其实是一种数学方法,只是套用到 FP 来用,他的重点在於将 function 的「多个参数转成单个参数」。

直接举个例,原本要把两个数字相加,需要带两个参数进去:

const addNum = (x, y) => x + y;

使用 currying 方法转换之後会变成

const addNum = x => y => x + y;

有没有很眼熟?我们在 Day 8 - Functional Programming 初探 (1) 就看到这种写法了,但没有特别提 currying,因为我忘了当时主要在讲 HoF,但也透过同样一个范例了解到,其实我们能够使用 currying 方法,都多亏了 HoF 呢!

但这乍看之下完全是多此一举吧!要相加就丢两个参数啊,何必变成一个?

没错,如果我们真的只有「相加」一个函式,那大可用第一种方法就好,但当我们要开始写 FP,别忘记主体是 function,最小单位是 function,基本上做什麽都要从 function 去排列组合。

function 的拆解与组装

currying 绝对是 FP 非常重要的一步,因为有了 currying,我们才可以将 function 拆成比较小的积木,然後用这些积木去「组装」更多 function:

const addNum = x => y => x + y;

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

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

看起来很复杂,但如果发挥前面学到 Declarative 的精神,先不要那麽在意这些程序码每一步动作,而是先看出这些英文的意图

// 这边有个函式,addNum 应该就是把数字加起来吧?
const addNum = x => y => x + y;

// 这边有个变数不知道存什麽东西,但它叫 addFive,应该就是用来 +5 吧?
const addFive = addNum(5);
// 这边有个变数不知道存什麽东西,但它叫 addTen,应该就是用来 +10 吧?
const addTen = addNum(10);

// addFive 是 +5,还放了一个 3 进去,那大概是 8 吧?
addFive(3); // 8
// addTen 是 +10,还放了一个 3 进去,那大概是 13 吧?
addTen(3); // 13

没错,英文不好来学 FP 好像真的比较吃亏XD

离题了,重点是透过 currying,我们可以创造小积木,然後把小积木组成中积木,最後再堆成一整个城堡(生产线)。

pipe & compose

「组合」function 的两大支柱,可以把两个或多个 function 组合成像生产线一样,A 执行完丢给 B,B 再接续执行。

直接上语法,如果要组合两个 function:

const pipe = (f, g) => (...args) => g(f(...args));
const compose = (f, g) => (...args) => f(g(...args));

pipe 是先左再右,compose 是先右再左,所以两者基本上是挑一个来用即可:

const pipe = (f, g) => (...args) => g(f(...args));
const compose = (f, g) => (...args) => f(g(...args));

const multiplyBy3 = num => num * 3;
const makePositive = num => Math.abs(num);
const multiplyBy3AndPositive = pipe(multiplyBy3, makePositive);
// 等同於
// const multiplyBy3AndPositive = compose(makePositive, multiplyBy3);

multiplyBy3AndPositive(-5); // 15

可以看到 multiplyBy3AndPositive 是一个完全透过「组装」制造的 function,有顺序地执行里生产线里面的两个 function,开始有 FP 的感觉罗!

这部份我们明天会有大量可以实战的案例!敬请期待

结语

今天介绍的东西其实满难的,一方面是因为 FP 本来就充满了许多数学元素,另一方面,当这些都只是概念的话,也很难跟实际的案例结合,但直接上实战又好容易死在路上QQ

所以今天先介绍概念们,明天我们集合这两天的大成,试着应用在实战吧!

从合成到分离
从原子到宇宙
连结了散落一地的星点

参考资料

hannahpun - Function Programming In JS


<<:  [Day 10] 测试串接

>>:  Day9 主动情蒐-nmap(1)

iOS App开发 OC 第二天, 属性 @property 的特性(attribute)

从Swift 到 OC 第二天, 属性 @property 的特性(attribute) tags:...

[DAY7]将范例上传(1)

上传LINE提供的范例回声机器人 第一步:先至LINE提供的GITHUB位置下载其资料夹,此处我们用...

[Day 18] Reactive Programming - Reactor Test(VirtualTime)

前言 接续上一篇介绍测试,之前也提到过Reactor提供VirtualTimeScheduler来让...

[Android Studio 30天自我挑战] CardView元件介绍

CardView卡片怖局是Android 5.0系统引入的元件, 继承自Framelayout,所以...

Day8# Array & Slice(下)

昨天没有写完的 Array & Slice(上) ,今天要来把补完进度。 那我们就开始吧 ─...