yo, what's up
Ramda 是一个 Functional Programming 的函式库,而 Ramda 的所有函式都有自带 currying.
笔者一开始学 Functional Programming 的时候,觉得 Ramda 跟 lodash 其实很像阿,一度非常不适应!!而且lodash 的 get 这麽好用,为何要改用 path 跟 prop ,超难用的。
到最後才发现,其实 Ramda 有很多非常好用的功能是 lodash/fp 没有的,如 transduce
跟 lens
等, 且所有函式都是 data last 以及自带 currying!
今天要介绍一下 Ramda 里一些实用的函式
可以看到上图,R.converge(Fn, [fn1, fn2])
,data 会传入 fn1, fn2 进行运算,最後作为 Fn 的参数,再运算出最终结果。
举例,现在有一个需求,是需要将 API 回传的格式,用 pdfmake 转成 pdf 档,所以我们需要做一层 refine 层
API format
[
{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
}
]
pdfmake format
{
content: [
{
style: 'tableExample',
table: {
headerRows: 1,
body: [
[
{text: 'albumId', style: 'tableHeader'},
{text: 'id', style: 'tableHeader'},
{text: 'title', style: 'tableHeader'},
{text: 'url', style: 'tableHeader'},
{text: 'thumbnailUrl', style: 'tableHeader'}
],
[
1,
1,
"accusamus beatae ad facilis cum similique qui sunt",
"https://via.placeholder.com/600/92c952",
"https://via.placeholder.com/150/92c952"
],
]
},
layout: 'lightHorizontalLines'
}
]
}
读者们可以练习看看
此时我们可以透过 R.converge
去做 refine
const format = val => ({ text: val, style: 'tableHeader' })
const refine = R.converge(
R.pair,
[
R.compose(R.map(format), R.keys, R.head),
R.map(R.values)
]
);
const toPDFFormat = data => ({
content: [
{
style: 'tableExample',
table: {
headerRows: 1,
body: refine(data)
},
layout: 'lightHorizontalLines'
}
]
})
用图说明上面的流程,首先 data 会分别传入 R.compose(R.map(format), R.keys, R.head)
以及 R.map(R.values)
进行运算,将 key 与 value 改写跟取出成 pdfmake 所要的格式, 最终再进行合并。
identity
, 在 Functional Programming 是一个使用频率很高的函式,实作也非常简单
const identity = x => x;
对,没错,就是将传入的参数原封不动的回传,想必读者们可能会开始纳闷,这个函式到底可以用在那些地方。
笔者一开始看到 identity
是在 Functional-Light-JS,其范例是将其作为 predicate 函式,举例
import * as R from 'ramda'
const wordArr = ['', 'hello', 'world'];
wordArr.filter(R.identity); // ['hello', 'world']
而以下是笔者在专案中使用到的情境
在进行链式 (chainable) 写法时,非常适合将函式 default value 设定为 identity
举例来说,现在有查询使用者资料的模组,此一模组负责验证,送出请求...等, 而所有用到此一业务逻辑的页面都会用此一模组
const queryUser = (url, schema, options) => {
/** ... other logic ... */
const submitProcess = (data) =>
schema
.validate(data)
.then(d =>
fetch(url, { body: JSON.stringify(d), method: 'POST'})
)
.then(successHandler)
.catch(console.error)
/** ... other logic ... */
return {
submit: submitProcess
}
}
假设现在想要在验证结束後,送出请求前对送出资料进行 refine, 这时候我们就可以在多新增一条 .then(refineAfterValidated)
,并且为了不要动到其他已用此一模组的服务,我们就可以给refineAfterValidated
预设为 identity ,这样既不会影响现有服务,也可以扩充新需求。
const queryUser = (url, schema, options) => {
const { refineAfterValidated = identity } = options;
/** ... other logic ... */
const submitProcess = (data) =>
schema
.validate(data)
.then(refineAfterValidated)
.then(d =>
fetch(url, { body: JSON.stringify(d), method: 'POST'})
)
.then(successHandler)
.catch(console.error)
/** ... other logic ... */
return {
submit: submitProcess
}
}
这个概念其实源自於 Functional Programming 的 Applying,是一个非常重要的概念,也会在之後的文章讲到。
现在读者们可以思考一件事情,当两组 Array 内的值要进行相加时,我们会怎麽做?
const add = (x, y) => x + y;
add(Array.of(1), Array.of(1)) // "11"
结果出现了 11
,为什麽呢? 因为 Array 内值的就像是在一个 container 内,我们必须将它取出才能进行运算,所以我们需要先将值透过 map 取出来,才能进行运算
Array.of(1).map(x => Array.of(1).map(y => add(x, y))) // [[2]]
// same as
[1].map(x => [1].map(y => add(x, y))
但问题似乎还没有解决,现在的值反而被两层 Container(Array) 包覆住了,所以我们需要压平它
Array.of(1).flatMap(x => Array.of(1).map(y => add(x, y))) // [2]
这样看起来非常的复杂,lift 就是用来解决这个问题的勇者,其概念就是 Applying,之後会在 Applying 的时候讲解!
import * as R from 'ramda'
R.lift(R.add)([1], [1]) // [2]
这样是不是变得非常乾净!!!!!
但要注意放入
lift
跟liftN
的函式都必须是 currying 化的函式!
而另外一个实用的小技巧就是有需要进行排列组合的运算时,也可以使用 lift
,
const color = ['Black', 'White'];
const size = ['S', 'M', 'L'];
const result = lift(pair)(color, size)
// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]
也可以用相同概念 liftN
去改写
const color = ['Black', 'White'];
const size = ['S', 'M', 'L'];
const combine = [color, size]
const result = liftN(combine.length, pair)(...combine)
// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]
而其实 liftN
底层时做就是用 ramda 的 ap
跟 map
进行实作,我们就来简单实作一个简易版的 liftN
import * as R from 'ramda';
const lift2 = R.curry((g, f1, f2) => R.ap(R.map(g, f1), f2))
const liftN_ = R.curry((arity, fn, list) =>
list.slice(1, arity)
.reduce((acc, val) => ap(acc, val), R.map(fn, list[0])))
liftN_(2, pair, [color, size])
// [["Black", "S"], ["Black", "M"], ["Black", "L"], ["White", "S"], ["White", "M"], ["White", "L"]]
当日常开发中,遇到连续使用 .map
.filter
以及 .reduce
的情境时,就非常适合用 transduce 去进行优化,由於有几个 .map
跟 .filter
时间复杂度就会多几个 O(n),而 transduce 就是将其优化到无论现在有几个 .map
跟 .filter
时间复杂度就是固定的O(n) ,之後会在未来的文章内提到实作方法。
假设我们现在有一组阵列,要乘三後取偶数,原本写法
const tripleIt = x => x * 3;
const isEven = (num) => num % 2 === 0
const arr = [1, 2, 3, 4]
// 原本写法
arr.map(tripleIt).filter(isEven) // [6, 12]
用 transduce後写法
import * as R from 'ramda'
const transducer = R.compose(R.map(tripleIt), R.filter(isEven))
R.transduce(transducer, R.flip(R.append), [], [1, 2, 3, 4]); // [6, 12]
在 Function Composition 的时候有提到如何对 compose 进行 debug
而 tap
就节省了我们写 log
的时间,所以可以将上次范例改写
import * as R from 'ramda'
const makeItalianHerbalBreast =
compose(
R.tap(console.log),
wrapIt,
R.tap(console.log),
addFlavor('italianHerbal'),
R.tap(console.log),
grab('breast'),
R.tap(console.log),
)(WHOLE_CHICKEN)
// [ 'leg', 'wing', 'breast', 'buttock', 'breast strips', 'land', 'bug' ]
// breast
// italianHerbal breast
// Wrapped(italianHerbal breast
感谢大家的阅读!
NEXT: Lense
>>: 【Day 06】 实作 - 设定 Google Analytics 工具查看 WordPress 网站
补充一点HTML的资讯,HTML从1995年至今已经发展了多个版本,目前主流使用为HTML5,每个版...
今天本来要照着顺序来聊聊开发环境介绍的,但太枯燥了先跳过吧XD 想说点简单的,顺便为下一篇内容提出几...
Day14 - 谈谈有关情绪 在《真实的幸福》这本书里面,提到了积极情绪跟消极情绪的使用方法。 积极...
前面讲了很多 Kotlin Exposed 框架使用的方式。 今天来讲点观念性的东西,谈谈 Expo...
遍历便利 | 细数每个item | 不擅长读空气? var支援你 ...