前言
没看过前几章的请先去专栏看前两章的内容,因为一章讲不彻底...
彻底搞懂 Functional Programming (一)
彻底搞懂 Functional Programming (二)
✅ 实战 - 购物车流程
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,如果忘记他在干嘛,可以参考 彻底掌握 Function (二),就记得它是用来组合积木、组合函式用的,把里面的所有函式组成一个生产线,而且是由左到右的顺序。
接着是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 成功组合了,但总共有四个啊! 如何组合多个 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
}]
};
嗯。。。虽然答案正确,但看起来很怪对不对?
好像明明只要一条生产线一次做完的事情,却硬是切成两条生产线,等第一条完成接着做第二条,非常多此一举。
✔接受多个 function 的 pipe
所以我们试着改良一下生产线 pipe: 所以我们试着改良一下生产线 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),而且每个积木都专业分工,就可以用各种方式去组合它,有无限多可能啊!
感觉像乐高广告
结语
结语.....其实是预告(被打),下次会讲OOP,下期见!