yo, what's up?
今天要来介绍 Functional Programming 重要的概念,Curry.
我们先来看一个简单的函式 add
const add = (x, y) => x + y;
这应该就是大家所熟悉一般函式的写法,当我们进行两数相加时,只需要放入函式所需的参数,即可进行呼叫。
add(1, 1) // 2
简单吧! 看起来没有什麽好挑剔的,那如果多放或少放参数呢?
add(1) // NaN
add(1, 1, "Naurrr") // 2
多放参数也不是不行,只是会被忽视。 少放的话则会发生问题,变成 1+undefined
,则为 NaN
.
可以看到上面的范例,如果少放参数,函式也会立即被呼叫,且少放的参数全部会变成 undefined
, 那要怎麽让函式收到全部参数再呼叫呢?
相较於一般函式写法不同,Currying 这个概念在於将原本预期是要传入多个参数的函式 转化成 每次只接收一个参数作为输入,并回传新的函式,且等待下一个参数传入,直到函式所需参数皆到齐时,才会呼叫。
所以在 Currying 下的 add
函式写法就会是
const addC = x => y => x + y;
这时 addC
需要这样呼叫 addC(1)(1)
,好处就是我们可以不用一次放入全部参数,可以最大化函式的自由度,也就是说可以在任何时间与任何地点(不同档案路径) 呼叫 currying 的函式。
举个简单的例子:
// utils.js
export const add1 = addC(1);
// example.js
import { add1 } from './utils';
[1, 2, 3, 4, 5].map(add1) // [2, 3, 4, 5, 6]
我们在 utils.js
写了一个 add1
的函式,并在 example.js
引用,这样写大大地增加了写程序的自由度,以及复用性。
既然知道 currying 这个概念了,直接来实作一个可以将各种函式 currying 的通用函式。
function curry(fn, arity = fn.length) {
return (function nextCurried(prevArgs) {
return function curried(nextArgs) {
const args = [...prevArgs, nextArgs];
if(args.length >= arity){
return fn(...args);
}
return nextCurried(args)
}
})([])
}
现在就来将原本的 add
透过 curry
变成有 currying 特性的 add
吧 !
const add = (x, y) => x + y;
const addC = curry(add);
addC(1)(1) //2
分析一下 curry 这个函式是怎麽将各种函数通用化的,举 add(x, y)
来说,
一开始 curry(add)
传入时会马上呼叫 nextCurried
并且 prevArgs
的初始值为 []
,
当 1
被传入时,此时 args
会变成 [1]
,此时 args.length
还小於 add
传入参数长度(2), 所以会进行递回 nextCurried([1])
。
当另一个 1
被传入时,此时 args
会变成 [1, 1]
,而 args.length
也是 2
, 故收到所有参数的 add
会立即执行,并回传 2
。
或许大家会觉得 add(1)(1)
有点不方便,那有麽方法可以让更有弹性的,不管写成 addC(1)(1)
或是 addC(1, 1)
都可以执行。好消息是 ramda 或是 lodash/fp 的 curry
函式皆有支援,也有人称此为 loose curry
function looseCurry(fn, arity = fn.length) {
return (function nextCurried(prevArgs){
return function curried(nextArgs){
const args = [...prevArgs, ...nextArgs];
if(args.length >= artiy){
return fn(...args);
}
return nextCurried(args)
}
})([])
}
const add = (x, y) => x + y;
const addC = looseCurry(add);
这样我们就可以同时使用 addC(1)(1)
与 addC(1, 1)
的写法了!
看完上面的例子,看不太出来 curry 的好处,心想 so what...? 的读者们,接下笔者来会用实际范例来呈现 curry 的威力!
想像一下,现在 PM 要你处理这支 Users API,需求是
所需要的函式会有
// sort
const sort = (fn, data) => [...data].sort(fn)
// get
const get = (key, data) => data[key]
// concat
const concat = (symbol, data) => data.concat(symbol);
// map
const map = (transformer, data) => data.map(transformer)
重点一: 可以看到上面的都有一个共通点,就是 Data Last,这也是 ramda 或是 lodash/fp 的特色,将"需要处理的资料"放在函式的最後一个。晚点也会提到这样有什好处。
由於函式没有进行 currying 之前,函式需要一次传入其所需之参数
fetch('https://jsonplaceholder.typicode.com/users')
.then(r => r.json())
.then(data => sort((a, b) => b.address.geo.lat - a.address.geo.lat, data))
.then(sortedData => map((data) => get('username', data), sortedData))
.then(usernames => map((username) => concat('!', username), usernames))
.then(result => console.log(result))
.catch(err => console.error(err));
接下来,读者可以试着把所有函式进行 currying,并且重构上面的那段程序码
// 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(xs => sort((a, b) => b.address.geo.lat - a.address.geo.lat)(xs))
.then(xs => map(get('username'))(xs))
.then(xs => map(concat('!'))(xs))
.then(result => console.log(result))
.catch(err => console.error(err));
可以看到跟上面的写法看似没有什麽差别,但是我们可以将写法进一步改成
fetch('https://jsonplaceholder.typicode.com/users')
...
.then(sort((a, b) => b.address.geo.lat - a.address.geo.lat))
...
而这种写法又称为 point-free style. 接下来用 point-free style 重构上面的那段程序码
发现了吗! Isn't that neat!!??
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)
由上面笔者整理一些重点
1. 易读性增加
有些读者们可能会认为,可以将所有 .then
要做的事,再抽出来变成一个函式,如下面范例
const getNameFromSortedData = sortedData =>
map(data => get('username', data), sortedData)
fetch('https://jsonplaceholder.typicode.com/users')
...
.then(getNameFromSortedData)
...
可以试想一下,这样不是就需要帮每个函式进行命名,然而命名不是一件容易的事情,面对不容易的事情,其中一个方法,就是不要去做这件事。
对! Currying 过後的函式,就已经可以清楚表明程序码到底在干嘛,就不太需要再包成一个函式,并且为函式命名。
fetch('https://jsonplaceholder.typicode.com/users')
...
.then(map(get('username')))
...
2. Data Last
其实这点是非常重要的,Data Last 就是将需要处理的资料作为函式的最後一个参数。
function(fn, data)
这样写有什麽好处呢?
其可以最大化 currying 的效果! 再透过 point-free style 可以让阅读程序的人只需要专注於这段是处理了什麽逻辑!
NEXT: Function Composition
>>: Day04: 04 - 页面刻划(3) -商品详情、订单详情、个人资料
We are making your quest for VIP coats simpler by ...
历行性的邮件报告,主旨和内容,一样的都要再打一次,要找之前那封,不好找.很花时间. 想到一个方法 历...
今天的内容为该如何制作血量,并且在攻击时或受到伤害时改变血量。 ...
Vaadin Start Vaadin 官方网站提供快速产出程序码工具,所见即所得,还可设定权限,分...
最後一篇,持续来介绍 VSCode 的套件,让你靠一套文字编辑器在路上横着走! CodeSpellC...