内容简介:不久前我在掘金发表了文章Ramda 有两个特性让它从其它工具库中脱颖而出:这两个特征让我们可以很轻松利用 Ramda 写出 "point free" 风格的代码。所谓 point 就是指作为参数传进函数的数据。point free 就是脱离数据的代码风格。通过做到 point free,我们做到了
不久前我在掘金发表了文章 如何在 JS 代码中消灭 for 循环 。文中我写了很多 工具 函数,那些工具函数都能达到设计的目的。但是,很重要一点我还没告诉读者,其实我是几乎不用自己写的那些工具函数的:joy: 不是因为怕自己写错了,而是有更强大好用的替代方案。我今天就介绍一个我用的最多的工具函数库 Ramda,展示怎样用 Ramda 写出既简洁易读,又方便扩展复用的代码。由于时间和精力有限,我就不解释我用到的每一个 Ramda 函数的用法了,大家感兴趣的话可以去查官方文档。
Ramda 有两个特性让它从其它工具库中脱颖而出:
- 所有 Ramda 函数都已经被柯里化。
- 所有 Ramda 函数都把数据作为最后一个参数传入。
这两个特征让我们可以很轻松利用 Ramda 写出 "point free" 风格的代码。所谓 point 就是指作为参数传进函数的数据。point free 就是脱离数据的代码风格。通过做到 point free,我们做到了 行为和数据的分离 ,这利于我们写出更安全(组合行为时没有副作用),更易扩展(脱离数据的逻辑容易复用),和更易理解(读高阶函数的组合就像读普通英文一样)的代码。
一,第一个 point free 例子
如果你写过 React + Redux 项目,你可能经常写这种代码:
function mapStateToProps(state) { return { board: state.board, nextToken: state.nextToken } } 复制代码
我们可以用 Ramda 的 pick
函数改写一下:
import { pick } from 'ramda'; function mapStateToProps(state) { return pick(['board', 'nextToken'], state) } 复制代码
继续改写成 point free 风格,可以写成这样:
const mapStateToProps = pick(['board', 'nextToken']); 复制代码
是不是简洁了很多?
二,函数组合
问题:有这样一个数组:
const teams = [ {name: 'Lions', score: 5}, {name: 'Tigers', score: 4}, {name: 'Bears', score: 6}, {name: 'Monkeys', score: 2}, ] 复制代码
要求找出分数最高的小组,并取到名字。
答案:
import { compose, head, sort, descend, prop } from "ramda"; const teams = [ {name: 'Lions', score: 5}, {name: 'Tigers', score: 4}, {name: 'Bears', score: 6}, {name: 'Monkeys', score: 2}, ]; const sortByScoreDesc = sort(descend(prop("score"))); const getName = prop("name"); const findTheBestTeam = compose( getName, head, sortByScoreDesc ); findTheBestTeam(teams) // => Bears 复制代码
稍微感受一下。数据是在最后一步才提供的,提供目标数据之前一直在组合行为,没有改变数据,没有任何副作用。注意 compose
是从后往前组合函数,如果习惯从前往后组合函数,用 pipe
。
再来一个:
问题:把下面这个查询字符串转成对象:
const queryString = "?page=2&pageSize=10&total=203"; 复制代码
答案:
import { compose, fromPairs, map, split, tail } from "ramda"; const queryString = "?page=2&pageSize=10&total=203"; const parseQs = compose( fromPairs, map(split("=")), split("&"), tail ); const result = parseQs(queryString); // => { page: '2', pageSize: '10', total: '203' } 复制代码
你可能会问,JS 原生都提供 map
方法了,为什么还有用 Ramda 的? 原因是文章开头提到的,Ramda 函数有两个特厉害的属性。这个例子里,给 map
传一个回调函数,它会返回一个新函数,等你给它传数据。
三,一个数据用两次,怎么 point free?
有时候会遇到这种情景,根据一个数据算出结果,再根据相同数据算出另一个结果,然后把两个结果进行某种运算。比如这个简单例子:
问题:给定一个用户对象,根据用户 id 生成头像地址,并把地址合并到用户对象上。
// 合并前 const user = { id: 1, name: 'Joe' } // 合并后 { id: 1, name: 'Joe', avatar: 'https://img.socialnetwork.com/avatar/1.png' } 复制代码
答案一:
const generateUrl = id => `https://img.socialnetwork.com/avatar/${id || 'default'}.png`; const getUpdatedUser = user => ({ ...user, avatar: generateUrl(user.id) }); getUpdatedUser(user); 复制代码
这个方案已经足够简洁,但是并没有达到 point free 的要求。数据 user 提前出现了,而我们期待的是在函数组合时不关心数据,哪怕是作为参数。但是,数据在计算过程中需要多次用到,怎样在没有数据(连代表数据的参数都没有)的情况下表达对未来数据的多次操作?Ramda 提供的 converge
函数可以解决这个问题:
答案二:
import { compose, converge, propOr, assoc, identity } from "ramda"; const user = { id: 1, name: "Joe" }; const generateUrl = id => `https://img.socialnetwork.com/avatar/${id}.png`; const getUrlFromUser = compose( generateUrl, propOr("default", "id") ); const getUpdatedUser = converge(assoc("avatar"), [ getUrlFromUser, identity ]); getUpdatedUser(user) 复制代码
converge
函数接受两个参数,第一个参数是最终执行的函数,第二个参数是由作用于传入数据的变形函数组成的数组。在这个例子里, user
数组先分别传给 identity
和 getUrlFromUser
函数,然后把这两个函数的计算结果分别传给 assoc("avatar")
。 identity
可能是最无聊的函数,它长这样:
const identity = x => x; 复制代码
我们要保留 user
数据不动,然后传给 assoc("avatar")
作为第二个参数,所以用了 identity
。
四,方法和数据耦合在一起,怎么 point free ?
有些时候方法就在数据上。比如用 jQuery 选中 DOM 元素后,对 DOM 元素进行操作的方法。假设 DOM 上有个 <div id = "el1"></div>
,用 jQuery 选中元素后,执行某个动画效果:
$('#el1') .animate({left:'250px'}) .animate({left:'10px'}) .slideUp() 复制代码
jQuery 的方法全在选中 DOM 元素后生成的对象上,方法是没法离开数据的。但这并不影响我们在数据还没给到之前组合行为。Ramda 提供了 invoker
函数解决类似问题:
import { invoker, compose, constructN } from "ramda"; const animate = invoker(1, "animate"); const slide = invoker(0, "slideUp"); const jq = constructN(1, $); const animateDiv = compose( slide, animate({ left: "10px" }), animate({ left: "250px" }), jq ); animateDiv("#el1"); animateDiv("#el2"); 复制代码
invoker
函数接受3个参数。第一个参数表示要在对象上执行的函数接受多少个参数,第二个参数表示要在对象上执行的函数的名字,第三个参数是目标对象。 constructN
是用来实例化一个构造函数或者类。
五,强大的 lens
lens
是从函数式编程语言借来的一个概念,它相当于对某个属性的聚焦。比如 lensProp('a')
,就是对 a 属性的聚焦,不管这个 a 属性由哪个对象提供。聚焦之后,我们可以很方便的读取属性( view
)和改变属性( over
)注意,Ramda 中所有改变值的操作都不是真的在原数据基础上改,而是返回改了指定属性的新值。
举个很简单的例子。
import {lensProp, view, over, toUpper} from 'ramda'; const person = { firstName: 'Fred', lastName: 'Flintstone' } const fLens = lensProp('firstName') const firstName = view(fLens, person) // => 'Fred' const result = over(fLens, toUpper, person) // => {firstName: 'FRED', lastName: 'Flintstone'} 复制代码
上面例子还不能看出 lens
有什么用。来看下实际使用场景:
问题:
用 React 写一个简单 counter demo,点击 + 和 — 按钮时,计数器对应加 1 和减 1。
lens
用在 React 的 setState
里非常方便:
import {inc, dec, lensProp, over} from 'ramda' const countL = lensProp('count') const transformCount = over(countL) const incCount = transformCount(inc) const decCount = transformCount(dec) // ... 其它细节 state = { count: 0 } increase = () => { this.setState(incCount); }; decrease = () => { this.setState(decCount); }; // ... 其它细节 复制代码
lens
与 React 的配合,最能发挥作用的情景是在写函数式组件的时候。有兴趣可以参考这个Demo
六,更高阶的函数组合
前面提到的内容都是常规的函数组合,Ramda 还提供了 monad 的组合。抱歉要扔术语了,如果有时间,未来我可能会解释什么是 monad。大家常用的 Promise 就是个 monad,通过运行 Promise 得到值之后,你并不能在 Promise 外面操作值,而是必须在 then
方法里面处理。这就是 monad 的最大特征(它里层会返回同样的 Type,一层层嵌套,必须通过某种 flatMap 机制将里层的值取出)。
首先,最常见的是组合 Promise。来看例子。
假设我们先根据用户 email 地址请求得到用户信息,然后再根据用户 ID 得到用户粉丝数。
// 获取用户信息的异步函数 const ajaxForUserInfo = userEmail => fetch(/* post request */); // 获取用户粉丝的异步函数 const ajaxForUserFollowers = id => fetch(/* post request*/); const fetchUserFollowers = async userEmail => { const userInfo = await ajaxForUserInfo(userEmail); const userFollowers = await ajaxForUserFollowers(userInfo.id); return userFollowers; }; 复制代码
用 Ramda 提供的 composeP
,可以组合上面两个 Promise:
import {composeP} from 'ramda'; const ajaxForUserFollowers = composeP( ajaxForUserFollowers, ajaxForUserInfo ); const fetchUserFollowers = async email => { const userFollowers = await ajaxUserFollowers(email); return userFollowers; }; 复制代码
上面例子只有两个 Promise 需要组合。如果是多个的话,组合的优势更明显。
高能预警:
上面的内容已经足以覆盖大部分函数组合的需求。接下来要讲的一种函数组合算是比较硬核的函数式编程了,感兴趣的可以接着看,没兴趣的可以跳过了。
除了对 Promise 的组合,Ramda 还提供更泛的 monad 组合,叫 Kleisli Composition。暂时不用知道这玩意是什么,知道它是对各种 monad 的组合就行了。我们以 Maybe Monad 为例来看 Kleisli Composition:
可能读者在看到函数组合时会有疑问,如果某个函数有可能返回空值,还怎么组合?在每个后续函数前都做空值判断?那就真不优雅了。函数式编程提供了 Maybe Monad 进行空值处理,Maybe 可以和其它 monad 正常组合。
来看例子:
假设我们有这样一个 JSON 字符串需要解析
'{"user": {"address": {"state": "in"}}}' 复制代码
我们需要取到用户的任何一个深度的地址,而每一层获取地址都可能失败。所以,每一次取值,我们都要做空值处理。
// 解析 JSON 的函数,做了错误处理 function parse(s) { try { return JSON.parse(s); } catch (e) { return null; } } 复制代码
import Maybe from "folktale/maybe"; import { compose, composeK, toUpper } from "ramda"; // 解析 JSON 可能返回 null,所以把结果放到 Maybe 里面 const maybeParseJson = json => Maybe.fromNullable(parse(json)); // 获取属性也可能返回空值,把结果放到 Maybe 里面 const maybeProp = prop => obj => Maybe.fromNullable(obj[prop]); // 传给 toUpper 的值可能不是字符串,也要把结果放到 Maybe const maybeUpper = compose( Maybe.of, toUpper ); // composeK 代表 Kleisli composition const getStateCode = composeK( maybeUpper, maybeProp("city"), maybeProp("state"), maybeProp("address"), maybeProp("user"), maybeParseJson ); const s = '{"user": {"address": {"state": "in"}}}'; getStateCode(s).getOrElse("Error ocurred"); // city 属性不存在,程序会返回 'Error ocurred' 复制代码
以上所述就是小编给大家介绍的《优雅代码指北 -- 巧用 Ramda》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员的数学思维修炼(趣味解读)
周颖 / 清华大学出版社 / 2014-4-1 / 45.00元
本书是一本专门为程序员而写的数学书,介绍了程序设计中常用的数学知识。本书门槛不高,不需要读者精通很多高深的数学知识,只需要读者具备基本的四则运算、乘方等数学基础知识和日常生活中的基本逻辑判断能力即可。本书拒绝枯燥乏味的讲解,而是代之以轻松活泼的风格。书中列举了大量读者都很熟悉,而且非常有趣的数学实例,并结合程序设计的思维和算法加以剖析,可以训练读者的数学思维能力和程序设计能力,进而拓宽读者的视野,......一起来看看 《程序员的数学思维修炼(趣味解读)》 这本书的介绍吧!