Day 04 - Function Composition

yo, what's up

今天就来谈谈 Functional Programming 的核心,Compose. 有了这个概念後,就可以把多个功能单一的函式组合成一个复杂的程序。

建议在看这篇文章之前先将前一篇的部分看完,因为 Function composition 跟 Curry 概念是相连的。

Functional Programming is all about composition.

为何麽需要 Compose?

接下来会用故事的方式,来讲解为什麽需要 compose.

小明观察近年健身风气开始盛行,所以看准鸡胸肉市场,决定开一家鸡肉工厂。起初的想法只是请员工帮忙从一只 全鸡上取出鸡胸肉,再进行 口味调配包装

首先需要一只全鸡!

// 这是我的鸡
const WHOLE_CHICKEN = ["leg", "wing", "breast", "buttock", "breast strips", "land", "bug"]

那从取出鸡胸肉到包装呢?

// 全鸡上取出鸡胸肉
const grab = 
    curry((part, chicken) => chicken.find(p => p === part));

// 口味调配
const addFlavor = curry((flavor, part) => `${flavor} ${part}`);

// 包装
const wrapIt = curry(item => `Wrapped(${item})`)

研拟好 SOP 後,小明鸡胸肉舖 终於风光开幕拉,找了占地 400 平的工厂,将机器线路拉好,并请大量的地方叔叔跟阿姨当员工,而生产流程一开始是这样的,

https://i.imgur.com/L4eJRQu.png

首先会有专门取鸡胸肉的员工,取好鸡胸肉後,走到腌制区交给腌制鸡胸肉的员工,该员工腌制好後,在走到包装区交给包装员工进行包装。

程序实践版:

const grabBreast = grab('breast', WHOLE_CHICKEN);

const addItalianHerbalFlavor = addFlavor('italianHerbal', grabBreast);

const product = wrapIt(addItalianHerbalFlavor)

一开始这个流程看似并没有问题,生意稳地的成长,并且在社群媒体流传了 吃鸡胸肉找小明 的优良风评。但好日子不长久,因为网路名人 "巨石贝多芬" 看到这个地段生意如此火热,决定也在这里开一家鸡胸肉铺,并且日产量是 小明鸡胸肉铺 的三倍,且更便宜。

看到生意开始衰退的小明大惊失色,开始与自家员工思考生产流程有没有可以优化的,而已经熟悉整个生产流程的地方叔叔与阿姨们点出当前制作流程的缺点。

"他们点出在制作过程中需要跨区移动,这过程太冗长了。其实可以把这整个制作流程串连在一起。"

所以小明决定听从建议,将生产流程串连再一起,这样员工就节省了移动时间

程序实践版:

const product = 
    wrapIt(addFlavor('italianHerbal', grab('breast', WHOLE_CHICKEN)))

https://i.imgur.com/i9sBchS.png

经过这次产品优化後,小明鸡胸肉铺 又回归到之前门庭若市的状态,但商场就像是爱情一样,总是变化多端。"巨石贝多芬" 开始卖起了各种口味的鸡胸肉,此举又将所有客人吸引过去。

小明知道此事後,决定将流程自动化,并且聘请机器制造专家,帮忙设计能制造不同口味的机器, 而此机器专家灵光一闪,想要制造一台可以制造机器的机器,经过日以继夜的研发,终於完成世纪之作,

程序实践版:

const compose = (z, g, f) => (x) => z(g(f(x)))

const makeItalianHerbalBreast = 
    compose(wrapIt, addFlavor('italianHerbal'), grab('breast'))

const makeBlackPepperBreast = 
    compose(wrapIt, addFlavor('blackPepper'), grab('breast'))

makeItalianHerbalBreast(WHOLE_CHICKEN);
makeBlackPepperBreast(WHOLE_CHICKEN)

什麽是 Compose ?

定义

compose 就是将多个函式组合成另一个新函式。

const compose = (z, g, f) => (x) => z(g(f(x)))

可以看到上面的例子,我们把 wrapIt, addFlavorgrab 这些功能单一的函式组合成一个更强大的函式。

一些小重点

  • 执行顺序:

compose 的执行方式是 从右到左,也就是在阅读 compose 时,从左到右,如同下面范例,

  1. 先取鸡胸肉 (grab('breast'))
  2. 用义大利香草腌制鸡胸肉 (addFlavor('italianHerbal'))
  3. 进行包装 (wrapIt)
const makeItalianHerbalBreast = 
    compose(wrapIt, addFlavor('italianHerbal'), grab('breast'))

示意图:

   compose(z,          g,        f)     (d)

// <-- z(g(f(d))) -- g(f(d)) -- f(d) --- d
  • 型别限制:

上一个函数输出值的型别 一定要等於 下一个函数输入值的型别。

也就是 d 要与 f 函式输入相同型别, 而其输出值 f(d) 要与 g 函式输入相同型别。

  • 结合律:
compose(f, compose(g, h)) === compose(compose(f, g), h)
  • declarative:

将函式进行 Compose 後,会更清楚知道程序做了什麽,不用管资料在每个阶段是什麽状态。 如同接水管一样,只要知道起点跟终点的位置,用水管串连起来,当水注入水管时,只要在终点看水有没有跑出来就好了,不用去管中间发生了什麽。

实作出通用的 Compose

实作部分主要是让读着们可以理解概念,在开发时还是以 production ready 的 library 为主,像是 Ramda, lodash 等

const compose = (...fns) =>
    fns.reduce(
            (acc, fn) => (...args) => acc(fn(...args)), 
            x => x
        )

Debugging

在进行 compose 的时候,不免会有遇到错误的情况,在这边也介绍在如何进行 debug.

compose 因为是将多个功能单一的函式拼凑出一个复杂的函式,但想要检视当资料经过各个函式後的结果,则是可以运用 log 这个函式去达成。为了省去空间,将用 comma opeator 去表示 log.

const log = (pos) => (x) => (console.log(pos, x), x)

在这里将上面提到的范例,进行追踪

const makeItalianHerbalBreast = 
    compose(
        log('4'),
        wrapIt, 
        log('3'),
        addFlavor('italianHerbal'),
        log('2'),
        grab('breast'),
        log('1')
    )(WHOLE_CHICKEN)


// 1 [ 'leg', 'wing', 'breast', 'buttock', 'breast strips', 'land', 'bug' ]
// 2 breast
// 3 italianHerbal breast
// 4 Wrapped(italianHerbal breast

相较於之前的写法,我们现在加入 log 去追踪每个函式执行後的结果。

这样在 debug 或是 理解函式时会非常有用,可以精准定位,并进行修复或探讨。

Pipe

pipe 其实就跟 compose 的概念一样,只是执行的顺序不一样,其顺序是 从右到左

        pipe(z,       g,        f)     (d)

// d ----- z(d) -- g(z(d)) -- f(g(z(d))) --->

以我们上面的范例,若用 pipe 去改写就会变成这样

const makeItalianHerbalBreast = 
    pipe(grab('breast'), addFlavor('italianHerbal'), wrapIt);

makeItalianHerbalBreast(WHOLE_CHICKEN)

So What...?

还记得昨天 PM 跟工程师讨论的需求吗?

今日的 So What 主题与就是延续昨日的需求,在上一篇我们最後将 Curry 概念实践在程序码上,让我们把记忆拉回到昨天最後写出来的程序码

// util.js

// sort
const sort = curry((fn, data) => [...data].sort(fn));
// get
const get = curry((key, data) => data[key]);
// concat
const concat = curry((symbol, data) => data.concat(symbol));
// map
const map = curry((transformer, data) => data.map(transformer));
fetch('https://jsonplaceholder.typicode.com/users')
  .then((r) => r.json())
  .then(sort((a, b) => b.address.geo.lat - a.address.geo.lat))
  .then(map(get('username')))
  .then(map(concat('!')))
  .then(console.log)
  .catch(console.error);

那我们打铁趁热,现学现卖一下,给大家一点时间,用 Compose 概念重构一下上面的程序码。(答案下面揭晓)

首先,因为笔者的个人偏好,先将排序那段程序包成函式

const sortLatitude = 
    sort((a, b) => b.address.geo.lat - a.address.geo.lat)

接下来用 compose 将改写

// index.js 
const responseHandler = compose(
  map(concat('!')),
  map(get('username')),
  sortLatitude
);

fetch('https://jsonplaceholder.typicode.com/users')
  .then((r) => r.json())
  .then(responseHandler)
  .then(console.log)
  .catch(console.error);

有感受到了嘛!!! Isn't that neat!!??

接下来还可以在一个地方进行优化,或许各位读者都发现了,

compose(..., map(concat(!)), map(get('username')), ...) 
    ===
compose(..., map(compose(concat(!), get('username'))), ...)

等式右边的写法不但更简洁,也更有效率。所以重构的最终版本终於出来了!

// index.js 
const responseHandler = compose(
  map(compose(concat('!'), get('username'))), 
  sortLatitude
);

fetch('https://jsonplaceholder.typicode.com/users')
  .then((r) => r.json())
  .then(responseHandler)
  .then(console.log)
  .catch(console.error);

透过不断的抽象化,可以很清楚地知道整个资料处理逻辑!!! 真的是太棒了,希望读者们也有跟笔者一样的感受!!!

小结

Function Composition 就像是乐高一样,每块乐高积木虽然只有单一形状,但却可以组出一个复杂的模型,而这个概念是 Functional Programming 的核心!

参考资源

Functional-Light-JS Ch.4

NEXT: Ramda


<<:  [Day 04] 部署模型的挑战 — 资料也懂超级变变变!?

>>:  [Day19] Flutter - Const: Shared(part3)

[DAY6]Channal access token是什麽?

对於网上可用的服务,通常使用通过ID和密码进行身份验证,以验证使用者是否有权使用该服务。LINE 开...

[CSS] Flex/Grid Layout Modules, part 13

单元对齐跟留白的部分今天会继续,定位的问题基本上不出乱子的话就如同昨天说明的。当然,如果再加上对齐跟...

Day 0x7 - Laravel 资料库连接设定、资料表规划

0x1 Laravel 资料库连接 请先确认 php.ini 的 pdo_pgsql extensi...

【Day12】Git 版本控制 - git checkout 移动 head

什麽是 head? 我们在 commit 版本时,其实是将档案 commit 到一条 branch(...

Day 30. 监控大挑战 - 以 Zabbix 为例 - 完赛

Hi 大家今天是第三十天了,要跟大家回顾与心得。 这次个规划的主轴呼应第一天提及的精神 从需求出发,...