yo, what's up
今天要在 FP 里很有用的概念 Lenses, 它可以减少在处理资料结构逻辑时的复杂度,并且可以写出更容易阅读以及更乾净的程序。
const user = {
id: 0,
name: "JingMultipleFive",
username: "jing.tech",
email: "[email protected]",
address: {
street: "Wall",
suite: "Abc 123",
city: ["Taipei", "Subic", "New York"],
zipcode: "242",
geo: {
lat: "-43.9509",
lng: "-34.4618"
}
}
}
假设目前有一个需求,其为对上列的资料结构进行
根据上面的问题,我们在实作前需要
将字串转大写的函式
const toUpper = str => str.toUpperCase()
读者们可以用五分钟想想实作方式!
在笔者还不认识 Lenses 这个概念前,马上想到的方法就是 shallow copy,直接深入资料结构,瞄准目标,大闹一番!!!!!!
首先我们先进行第一个需求 修改
// 将使用者居住城市中的第一笔资料改成大写
const modified_user = {
...user,
address: {
...user.address,
city: [
toUpper(user.address.city.slice(0, 1)[0]),
...user.address.city.slice(1)
]
}
}
然後 读取 该值
// 取出该值
modified_user.address.city[0] // TAIPEI
那我们来分析一下,上面的两个行为 读取 以及 修改,
modified_user.addresss.city[0]
// Uncaught TypeError: Cannot read properties of undefined (reading 'city')
有些读者可能会想,那用前几天提到的 function composition 呢?
R.compose(R.toUpper, R.path(['address', 'city', 0]))
这个作法只能将目标值取出,并修改,但并不会放回原本的资料结构内!
Lenses 是 FP 的工具之一,让开发者可以在复杂的资料结构中,对特定的子结构 (subpart) 进行 读取 / 写入 / 修改,其他更进阶的概念像是 Folds 跟 Traversals. 在之後的文章可能会提到。
Lenses 需传入两种 method, getter 以及 setter!
以下笔者将使用 Ramda 来 demo 这个概念
首先我们先使用 Ramda R.lens
,解决上面的问题
R.lens
R.lens(getter, setter)
getter: 取得目标资料。
setter: 写入目标资料,注意 setter 不能 mutate 原有的资料结构。
const R = require('ramda');
const getFirstCity = data => data.address.city[0];
const setFirstCity = (value, data) => ({
...user,
address: {
...user.address,
city: [value, ...user.address.city.slice(1)]
}
})
const firstCityLens = R.lens(getFirstCity, setFirstCity);
在先前我们提到 Lenses 可以对资料结构中特定的子结构 (subpart) 进行 读取/写入/修改。
而Ramda 对应的操作函式为
R.view(lens, dataStructure)
R.set(lens, updateValue, dataStructure)
R.over(lens, updateFunction, dataStructure)
// 取出该值
R.view(firstCityLens, user)
// 用 R.set, 将使用者居住城市中的第一笔资料改成大写
R.set(firstCityLens, R.toUpper(R.view(firstCityLens, user)), user)
// 用 R.over, 将使用者居住城市中的第一笔资料改成大写
R.over(firstCityLens, R.toUpper, user)
好的,...想必现在各位读者应该都心想 "So...What..., 怎麽这麽复杂,也没多乾净嘛!"。
修但几咧,这只是 demo 最阳春的 lens 如何使用,竟然都使用 Ramda 了,其他函式当然也要给它用好用满。
Ramda 也有提供相关的函式,去定位目标资料,如图
资料结构 | getter | setter |
---|---|---|
单层 | R.prop | R.assoc |
多层 | R.path | R.assocPath |
而目前我们要定位的 city 是在资料结构的深层,所以我们需要使用 R.path
以及 R.assocPath
作为 lens 的 getter 与 setter!
const getFirstCity = R.path(['address', 'city', 0]);
const setFirstCity = R.assocPath(['address', 'city', 0])
const lensCity = R.lens(getFirstCity, setFirstCity);
// 取出该值
R.view(lensCity, user)
// 将使用者居住城市中的第一笔资料改成大写
R.over(lensCity, R.toUpper, user)
R.lensPath
再改写一次可以看到 R.path
跟 R.assocPath
所放入的参数都是 ['address', 'city', 0]
,Ramda 内有提供 R.lensPath
去简化此写法。
R.lensPath(<prop>)
只是R.lens(R.path(<prop>), R.assocPath(<prop>))
的简写。
const lensCity = R.lensPath(['address', 'city', 0]);
// 取出该值
R.view(lensCity, user)
// 将使用者居住城市中的第一笔资料改成大写
R.over(lensCity, R.toUpper, user)
Isn't that neat!?
重点是 R.over
与 R.set
返回的值是 immutable 的,也就是不会去动到原有的资料结构!
Lenses 必定符合下列三个特性
1. set after get
view(lens, set(lens, a, store)) ≡ a
写入 一个值到资料结构中,然後立即 读取 该目标值,则会得到刚 写入 的值。
2. set after set
set(lens, b, set(lens, a, store)) ≡ set(lens, b, store)
写入 一个值到资料结构中,然後立即重复 写入 值到该目标结构中,则会得到刚 写入 的值,也就是 b。
3. get after set
set(lens, view(lens, store), store) ≡ store
当你 读取 资料结构中的目标值,然後立即 写入 值到该目标结构中,资料结构不变。
Lenses 就是一般的函式,而想到函式代表我们可以进行 composition!
唯一不同的是它们执行顺序是从左到右边 (而非先前所学的右到左),明天介绍的 transduce 也是从左到右边!
const address = lensProp('address');
const city = lensProp('city');
const head = lensIndex(0)
const lensCity = compose(address, city, head);
// 将使用者居住城市中的第一笔资料改成大写
R.over(lensCity, R.toUpper, user)
// 取出该值
R.view(lensCity, user)
其实单就操作资料结构,Lenses 只是其中一个方法,还有其他原生的 Web API 像是 proxy 或是三方套件像是 immer,又或是可以期待未来的 JS 新 feature Record,它们使用起来或许对於非函数式编程者会更友善,但 Lenses 不只提供 Functional 的方法操作资料结构,还有 Fold 跟 Traversables 之後也会一并讨论到。
Immer 范例
import produce from 'immer';
const modified_user = produce(user, draft => {
draft.address.city[0] = toUpper(draft.address.city[0])
})
// 取出该值
modified_user.address.city[0] // TAIPEI
感谢大家的阅读!
NEXT: Tranduce I
<<: [Day 06] tinyML的重要推手Arm Cortex-M MCU
>>: Day06 - 用 Next.js 做一个简易产品介绍页,使用 file-based routing
Object.prototype.call(变数)可以更详细地找出变数的型态 console.log...
今年年初,刚好被大学学长推坑 COSCUP 开发组,这次的官网是以去年为基础去做修改的,所以并没有花...
在Java程序设计中,有一个较为快速创造阵列的方法ArrayList,有别於固定大小的Array,A...
第一个要来看的公钥加密演算法是 RSA。 记得我们在 DAY6 的时候介绍到 RC4 时提到一个人吗...
LAST Day 终於到了铁人赛的最後一天,过程中复习了不少的东西,对某些用法有了更加的认识,过程中...