這兩天花了滿多心力在介紹 FP 的觀念跟方法,但其實大部分都停留在理論,或者教科書上的那種 apple、banana 的練習,大家都是在職場上走跳的,把理論轉為實務是必要的能力。
所以,今天我們試著結合前兩天講的東西,今天來寫一些實戰練習吧!
FP 最適合做那種「一連串」、「一個挨著一個」的程式,所以通常是有流程的,先做 A 再做 B 再做 C。
因此,我們來做一個購物車,模擬一個購物的流程,當然電商是很複雜的,我們只實作幾個主要的基本功能:
雖然已經定義出流程,但現在腦中就是一片空白,到底要從什麼東西開始寫啊?
首先要先定義「在生產線上跑」的資料,比如在水管這個生產線,「水」就是我們的資料;在廚房這個生產線,「青菜」就是我們的資料。
我們定義了:
const pipe = (f, g) => (...args) => g(f(...args));
const user = {
name: 'Joey',
cart: [],
purchases: []
};
const item = {
name: 'TV',
price: 24000
};
首先是 pipe
,如果忘記他在幹嘛,可以參考 Day 9 - Functional Programming 初探 (2),就記得它是用來組合積木、組合函式用的,把裡面的所有函式組成一個生產線,而且是由左到右的順序。
接著是 user
,我們定義一個用來代表使用者的物件,裡面的 cart
陣列代表購物車,而 purchases
陣列代表購買的項目。
最後是 item
,代表我們要購買的物件。
接著就要開始寫 FP 啦,FP 的最小單位是 function,而且每個 function 做的事情都盡可能少,專業分工,最後再透過 pipe
組合起來就可以了。
首先來寫「把商品加入購物車」:
const addItemToCart = (user, item) => {
return { ...user, cart: [...user.cart, item] };
};
接著要結帳了,先幫「商品價格打折」:
const applyDiscountToItems = (user) => {
const discount = 0.8;
const updatedCart = user.cart.map(item => {
return {
name: item.name,
price: item.price * discount
}
});
return { ...user, cart: updatedCart };
};
然後是「購物車結帳」,其實不會放進陣列就真的結帳開始出貨啦,不過那些與後端串接的邏輯就先省略了:
const buyItem = (user) => {
return { ...user, purchases: [...user.cart] };
};
最後是「清空購物車」:
const emptyUserCart = (user) => {
return { ...user, cart: [] };
};
有了上述四個積木,接著開始就是這三天鍛鍊下來的精華了!要來驗收囉!
首先,為了避免一開始就越級打怪,我們前面宣告的 pipe
只能夠接受 2 個 function 組合,所以我們先從前面 2 個開始:
const pipe = (f, g) => (...args) => g(f(...args));
const user = {
name: 'Joey',
cart: [],
purchases: []
};
const item = {
name: 'TV',
price: 24000
};
const addItemToCart = (user, item) => {
return { ...user, cart: [...user.cart, item] };
};
const applyDiscountToItems = (user) => {
const discount = 0.8;
const updatedCart = user.cart.map(item => {
return {
name: item.name,
price: item.price * discount
}
});
return { ...user, cart: updatedCart };
};
const newUser = pipe(addItemToCart, applyDiscountToItems)(user, item);
console.log(newUser);
執行結果
{
name: 'Joey',
cart: [{
name: 'TV',
price: 19200
}],
purchases: []
};
真的寫起來還是覺得很神奇,透過小 function 的組合出大 function,接著丟參數進去就跑出答案了,是不是很像數學在寫 f(x),還有 f(x)。g(x) 的感覺呢?
那現在問題來了,雖然兩個 function 成功組合了,但總共有四個啊!
最直覺的想法是再用一次 pipe:
const pipe = (f, g) => (...args) => g(f(...args));
// ... 中間省略,同上一個範例
const buyItem = (user) => {
return { ...user, purchases: [...user.cart] };
};
const emptyUserCart = (user) => {
return { ...user, cart: [] };
};
const newUser = pipe(addItemToCart, applyDiscountToItems)(user, item);
const newUser2 = pipe(buyItem, emptyUserCart)(newUser);
console.log(newUser2);
執行結果
{
name: 'Joey',
cart: [],
purchases: [{
name: 'TV',
price: 19200
}]
};
嗯。。。雖然答案正確,但看起來很怪對不對?
好像明明只要一條生產線一次做完的事情,卻硬是切成兩條生產線,等第一條完成接著做第二條,非常多此一舉。
所以我們試著改良一下生產線 pipe:
const pipe = (f, g) => (...args) => g(f(...args));
const newPipe = (...fns) => fns.reduce(pipe);
// 上面是比較好讀的版本,你也可以合併成一個:
const pipe = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));
可以看到 newPipe
的部分,它是一個 function,參數 ...fns
代表可以帶多個參數(逗點分開)進去,fns
進到函式內之後就會變成一個陣列(裡面存著你丟進來的多個參數)。
還記得 reduce
代表什麼嗎?它負責「整合」,目的是要把我們帶進去的多個參數整合,變成一個統一的生產線。
因此做完上述的改良,我們從原本只能組合 2 個 function,變成你帶幾個 function 進來,全都給你組成一條生產線。
所以我們的最終版本就出來了:
const pipe = (f, g) => (...args) => g(f(...args));
const newPipe = (...fns) => fns.reduce(pipe);
const user = {
name: 'Joey',
cart: [],
purchases: []
};
const item = {
name: 'TV',
price: 24000
};
const addItemToCart = (user, item) => {
return { ...user, cart: [...user.cart, item] };
};
const applyDiscountToItems = (user) => {
const discount = 0.8;
const updatedCart = user.cart.map(item => {
return {
name: item.name,
price: item.price * discount
}
});
return { ...user, cart: updatedCart };
};
const buyItem = (user) => {
return { ...user, purchases: [...user.cart] };
};
const emptyUserCart = (user) => {
return { ...user, cart: [] };
};
const newUser = newPipe(
addItemToCart,
applyDiscountToItems,
buyItem,
emptyUserCart
)(user, item);
console.log(newUser);
執行結果
{
name: 'Joey',
cart: [],
purchases: [{
name: 'TV',
price: 19200
}]
};
此時 FP 的好處立刻就出現了!
假如今天突然不想打折了,可以把 applyDiscountToItems
直接抽掉,程式立刻就按照你的意圖下去執行。
或者清空購物車之後還想做其它的事(比如返回首頁),就可以自己再寫個積木,然後放到 emptyUserCart
的後面即可,完全不用動到前面寫的函式們。
注意哦!這只是我們組出來的其中一種 pipe 而已,只要你手上有積木(function),而且每個積木都專業分工,就可以用各種方式去組合它,有無限多可能啊!
感覺像樂高廣告
FP 不是 Javascript 專屬的東西,而且已經非常有歷史了,因此網路上累積的學習資源非常豐富,這邊只用三天來討論完全是冰山一角,但我想足以讓完全沒概念的新手,有個初步對於 FP 的想像與理解。
我相信應該還是很多人看完通篇,仍然不知道 FP 在幹嘛,就跟我第一次學的時候一樣QQ,歡迎留言寫下你的疑惑,也許也是其它人的疑惑~
我相信學習不會有白費的過程,而是循序漸進的,即便不能一次讀懂,下次再讀到 FP 時,也就更有底氣了!
在寂靜與黑暗之中
閉上眼
已到了蟲洞外的新世界
Will - 理解函式編程核心概念與如何進行 JavaScript 函式編程
Les Lee - Functional Programming 一文到底全紀錄
newpipe 超讚的,而且可以這樣抽換 function 的彈性也很大(開放封閉原則)。
可以抽來抽去真的超好用的!真的像在組積木一樣~
看到 newpipe 的實作真是讓人感動,這就是我平常在用的 pipe 啊~~ 謝謝分享!
newPipe 第一次看到真的是看不懂,看懂了也不知道可以用來做什麼,真的是要自己來寫寫看才知道,我的收穫也很大!