今天會繼續來聊聊 FP 的一些重要觀念,而且會更偏向實際的做法,看看 Javascript 怎麼結合昨天聊到的 First-class、HoF、pure function,並且落實「以 function 為主體」來寫程式。
新手們通常看到這兩個單字就直接轉台了,如果你成功看到第二行,我會盡量講清楚來報答你(?)。
中文翻成命令式,是一個胼手胝足、努力向上的好青年,他做事的每一個步驟你都看在眼裡,他關注的是細節,是「如何做到」。
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;
}
});
中文翻成宣告式,是主管的類型,在公司的重要會議裡面,他只會告訴你大方向及一些策略,他關注的是整體,是「要做什麼」。
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 而已,沒有絕對正確的答案。
不過正是因為沒有正確答案,才更要去理解,這兩種的擅長點各自在哪,才能夠在對的地方發揮出來。
屠龍寶刀雖然厲害,但對於豬肉販來說,一般的鐵菜刀反而更順手。
如果我是豬肉販,我會選屠龍寶刀,然後賣掉買很多把鐵菜刀((拖走
籃球迷要瘋狂了!咖哩控也要暴走了!怎麼學 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 去排列組合。
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,我們可以創造小積木,然後把小積木組成中積木,最後再堆成一整個城堡(生產線)。
「組合」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
const pipe = (f, g) => (...args) => g(f(...args));
const compose = (f, g) => (...args) => f(g(...args));
你好~關於語法的部分,我知道args代表實際傳入function中的引數,...是展開運算子,但他倆組合起來我不太理解它在這個語法裡的作用
我嘗試拿掉後,發現這樣會報錯QQ
const pipe = (f, g) => g(f(...args));
const compose = (f, g) => f(g(...args));
謝謝!!!
你好:)
我們拆開來看會比較清楚一點:
先把引數跟展開運算子合在一起,印出來,會發現我丟 3 個引數,args 就會是一個長度為 3 的陣列
const test = (...args) => { console.log(args) };
test('Joey');
test('Joey', 123);
test('Joey', 123, 456);
執行結果
["Joey"]
["Joey", 123]
["Joey", 123, 456]
所以你在 pipe
看到的這個 g(f(...args))
,這個 args
就是一個陣列哦,它被展開之後丟進去 f
這個函式了
之所以加上 ... 運算子,是為了要讓 pipe 可以傳多個參數進去,反正不管丟幾個進去,都會變成一個陣列,生產線的長度就不會受限了~
不曉得這樣有解到疑惑嗎?這個 pipe 的函式是全場最難的大魔王,希望有任何疑問都可以提出唷~
謝謝你的解釋,覺得有再看懂一點點這個語法
之所以加上 ... 運算子,是為了要讓 pipe 可以傳多個參數進去,反正不管丟幾個進去,都會變成一個陣列,生產線的長度就不會受限了~
原本也有猜測是不是為了因應流程多於兩個步驟的狀況,所以需要展開運算子,謝謝你解開了我的疑惑。
想追問如果一個流程有三個步驟(或更多),那pipe的語法是否會變成:const pipe = (f, g, h) => (...args) => h(g(f(...args)));
這樣式子變得好難閱讀QQ,覺得應該有更好的寫法,所以想請教你,感謝~
看了其他文章,查到有人的寫法是這樣
const pipe =
(...functions) =>
(x) =>
functions.reduce((acc, fn) => fn(acc), x);
沒錯!你很會舉一反三耶,已經猜到隔天的內容了XD
我在 Day 10 就有帶到這個問題,如何從 2 個步驟,變成多個步驟,建議你可以先過去看看,用的方式跟你貼的是滿類似的,可以想想為什麼換成這種寫法,就可以從 2 個變多個?
composition 是 FP 很重要的核心,如果搞懂 pipe 怎麼用,對於學習 FP 是很大的幫助哦~
如果還是看不懂歡迎再發問唷~
對於生產線的我有點卡住煩請解惑:
1.請問如果函式中有涉及到與後端進行資料的戶傳,是會以promise then的流程去寫嗎?
2.以上述的例子有辦法與ajax進行實作嗎?
3.如果有3個函式,參數個數分別為1,2,3,請問這樣去拆分的時候仍然要以currying的特性,單獨拆成一項一項嗎?
這個好有難度喔
哈囉你好,我們有一個共通點,就是都覺得 FP 很難XD
這篇文章提到的 pipe 範例,只能夠給同步的函式使用,如果涉及後端資料傳送,便是非同步函式,肯定要用到 promise 與 async/await 的概念,可以參考這篇文章,pipe 會進化成這樣:
function asyncPipe(...fns) {
return async function(arg) {
let res = arg;
for (fn of fns) {
res = await fn(res);
}
return res;
}
}
而 currying 只是一種「將 function 的多個參數轉成單個參數」的方法,目的是透過拆解 function 成為小積木,再用這些小積木來組成其他東西。
不過 FP 不一定要把所有 function 都變成單個參數,單純是描述一種「以 function 為主體」的思考方式,所以可以自己根據狀況調整,只要可以透過 pipe 順利接起來,沒有硬規定一定要單個參數唷。
可以參考下一篇文章講實際的例子,或許會比較好理解唷!
了解~謝謝幫忙解惑~這類型的文章讀起來有難度 但懂了就覺得好方便~謝謝您
Hi,不知道作者還在不在,這段對新手來說有點複雜
const pipe = (f, g) => (...args) => g(f(...args));
不知道能不能理解成這樣? 感覺就是closure的概念嗎
const multiplyBy3 = num => num*3
const makePositive = num => Math.abs(num)
function result(x){
x = multiplyBy3(x)
return function(){
return makePositive(x)
}
}
console.log(result(-5)()) //15
你好,作者還活著~
的確 pipe 是使用 closure 的概念,初學的話也可以先用你寫的來理解,兩層的概念其實就是有個接力棒,A 計算完的結果,接力給傳給 B
不過基本上子 function(即 multiplyBy3
、makePositive
)會透過參數傳進去,以達到「組合」的功能。
這個範例只有兩個 function,如果需要組合三個甚至四個 function 時,展開就會變得相當複雜了,而這也是 carrying 使用的好時機。
BTW,在你寫的 code 中,x 代表帶入的資料,建議可以另外給一個 y 來代表 multiplyBy3
計算後的結果:
function result(x){
const y = multiplyBy3(x)
return function(){
return makePositive(y)
}
}
感謝回復!!
其實一直卡在,箭頭符號要return ...args這個問題
就這樣從昨天研究到現在終於成功展開了 QAQ
展開來對我來說就很好理解了 Orz
const pipe = function(f,g){
return function(...args){
return g(f(...args))
}
}
console.log(pipe(multiplyBy3,makePositive)(-5))
太好了!原來問題是因為沒有展開啊~
我一直隱約覺得沒有回答到你的問題,好險你成功理解了XD