Functional-Light JavaScript - Composition

前言

此為 frontend master 的 Functional-Light JavaScript, v3 的課程筆記,這篇講的是 composition,指的是如何把 function 結合,將小積木堆疊成大城堡的技巧。

內容不完全和課程編排相同,單純是個人消化過後的編排。

因為剛開始學 functional programming 的緣故,內容、觀念可能會有諸多錯誤,如有發現錯誤麻煩不吝指教,感謝!

抽象化(abstraction)

以 Kyle Simpson 的觀點來說,所謂 abstraction 指的是把兩個邏輯交織在一起的程式碼分離開來,讓我們可以關注各自的邏輯,而不是看一邊,還要顧及另一邊的邏輯。

We’re not abstracting to hide details; we’re separating details to improve focus.

先從改造巧克力工廠開始

提升產量

我們來假設一個情境,假設今天你是個巧克力工廠的顧問,負責改進整個巧克力工廠的生產流程,或大大小小的問題,總之老闆遇到問題就會去找你求救。

而你的興趣就是寫程式,有事沒事就會寫程式,還會從寫程式找出解決工作問題的靈感。好,我知道這假設有點微妙,可是先接受這個假設吧,拜偷。

接著我們看一段 code,這段 code 是要計算運費:

今天有段 code 是要計算運費:

const minus2 = (x) => x - 2
const triple = (x) => x * 3
const increment = (x) => x + 1

// add shiping rate
let temp = increment(4)
tmp = triple(tmp)
let totalCost = basePrice + minus2(tmp)

但我們看看就好,暫時先不談 code,而是先談一個生活化的情境。

截圖 2021-01-11 上午12.13.30

某天老闆跟你說:「我們的競爭對手產量遠超我們啊!快用你無敵的白金之星想想辦法!趕快增加產量超過競爭對手吧!」

你開始苦惱怎麼處理這問題,因為要增加產量又需要更多機器,可是廠房空間不夠,以目前的運作機制只能放一台機器,但不管了,煩惱放一旁,我們先回去寫程式。

接下來回到剛剛那段 code,我們把原本的程式改良成這樣:

const minus2 = (x) => x - 2
const triple = (x) => x * 3
const increment = (x) => x + 1

let totalCost = baseCost + minus2(triple(increment(4)))

我們簡化了原本較冗長的方式, increment(4) 執行完後的 output, 會被放進 triple 當作 input,triple 執行完的 output,又會被放進 minus2 當作 input,最終得到我們的結果。

想著想著,有發現提高巧克力工廠生產量的方法了嗎?

沒錯,如同我們的 function 生產完後的 output 會被當作下個 function 的 input 一樣,我們生產機器的 output 也可以當成下一個機器的 input 對吧,這樣就可以省去輸送帶佔的空間了!

更加抽象化

老闆某天說:

「現在的生產流程有點複雜欸,機器間的順序都要安排好,如果想調整就蠻麻煩的,所以我在想啊…」

「能不能造出一台懶人專用的的機器,我直接放巧克力進去,阿出來就是包裝完整的巧克力,聽起來很讚對吧!」

無奈之下我們依舊要回去寫 code 尋找靈感:

const minus2 = (x) => x - 2
const triple = (x) => x * 3
const increment = (x) => x + 1

function shippingRate(x) {
  return minus2(triple(increment(x))) // how to do
}

let totalCost = baseCost + shippingRate(4) // what to do

這段程式碼我們做了什麼?也沒什麼,就是把原本的計算變成 shippingRate 的 function,但邏輯來說變得更有意義。在 line 6 的地方,我們主要做的是「how to do」,指的是如何計算 shippingRate;而在 line9,我們做的則是「what to do」,指的是用 shippingRate 來做什麼。

這就是 Kyle 所指的 abstraction,我們分離出了「how to compute the shippingRate」以及「what to do with the shippingRate」

Declarative Data Flow

時光飛逝,巧克力工廠的生產流程經過你的改造後經營得不錯,只是又面臨了另一個問題,老闆某天又跑來說:

「上次的改造的確是不錯啦,工人只要按一個按鈕就可以生產出糖果,他也不用管理面在幹嘛,每次生產只要很簡單的按下按鈕就沒問題惹,只是調整機器細部還是稍嫌不方便。

而且最近我發現啊,我們的競爭對手居然每天都可以更新他們生產的巧克力品項欸??比如今天生產 A 巧克力,隔天他就能夠生產 B 巧克力!

所以,我更直接的說我的需求好了,你有沒有辦法…製造出一個能夠製造機器的…機器?這樣我們就可以辦到同樣事情惹!」

這是老闆理想的設計圖:

只要把小機器丟進超巨大機器裡,就可以幫我們把這些零件結合,變成新的機器,讚拉。

雖然聽起來很荒謬,但你依然決定走老樣子的路線,寫程式找改造機器的靈感。

function composeThree(fn3, fn2, fn1) {
  return function composed(x) {
    return fn3(fn2(fn1(x)))
  }
}

var shippingRate = composeThree(minus2, triple, increment)

let totalCost = baseCost + shippingRate(4)

從這段 code 你可以很清楚的明白,data flow 就是從右到左,從 incrementtriple 再到 minus2 不斷地經過加工,最後得到我們想要的 shippingRate。

Your programs don’t mean anything if they don’t mean data flow.

The whole point of your program is to have data coming in, doing stuff, and then going back out.

整個 data flow 變得 declarative,imperative data flow 的問題在於 data flow 的變化變得難以追蹤,declarative 則清楚許多。compose 在 utility library 都會提供此 API,如 Ramda compose

Associativity

// 結合律(associativity)
var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true

Compose 有結合律的特性,意指不管你將哪兩個分為一組都不重要,也就是說你的 code 可以像是下面這樣隨便組合:

compose(toUpperCase, compose(head, reverse));
// 或
compose(compose(toUpperCase, head), reverse);

又或者像是下圖:

截圖 2021-01-12 下午10.37.44

fp 看似不同的 function,但其實都是一樣的,只是 compose 結合 function 的方式不一樣而已,只要你的 function 順序一樣,因為 compose 符合結合律,所以你要怎麼 compose 都可以。

結合律的特性為我們提供了很大的靈活性,更能夠有效的運用 function,從小小小塊的樂高積木,逐漸蓋出一座大城堡。

Composition with Currying

function sum(x, y) { return x + y }
function triple(x) { return x * 3 }
function divBy(y, x) { return x / y }

divBy(2, triple(sum(3, 5))) // 12

假設我們今天想要把這段 code 用 compose 結合,會遇到什麼問題呢?就是 shape 不一樣的問題。上面幾個使用 compose 例子,傳入的每個 function 都是 unary 的,但有時候我們會遇到 shape 是 binary 的 function。

所以要是我們把 triple 的 output 放進 sum當作 input 就會出問題,因為 sum. 是 binary 的,兩者的 shape 對不上,而這時候就是 curry 要派上用場的時候惹!

function sum(x, y) { return x + y }
function triple(x) { return x * 3 }
function divBy(y, x) { return x / y }

divBy(2, triple(sum(3, 5))) // 12

sum = curry(sum)
divBy = curry(divBy)

compose(
  divBy(2),
  triple,
  sum(3)
)(5)

5 會被當作 input 傳入,之後就會順著 sum(3)tripledivBy(2) 的順序執行,這就是 curry 在 composition 的妙用,我們改變了 function 的 shape!讓它們變成 unary,就可以和其他樂高積木接上了。

References