原文信息
- 原文地址:Ranging Near and Far
- 作者:Scott Sauyet
- 发布日期:2014/09/17
声明
代码 100% 还原,翻译了部分注释。
直译感觉不妥的地方和几处换行按自己理解加工过,欢迎提出建议意见。
假如一个函数,有三个参数,其中第一和第三参数是可选的:
function f(a, b, c) { /* ... */ }
调用就传俩参数,你估计函数会怎么跑?
f(x, y);
--> {a == x, b == y, c == undefined, ...} // or
--> {a == undefined, b == x, c == y, ...} // or
--> // something weirder depending on the actual values of x, y?
很难啃腚吧?
我正试着写一篇文章,关于 Ramda 背后哲学,重点关注几个重要的特性,包括简洁,Mike 让我注意到另一个库的一个函数,它一点也不简单。Ramda 也有个类似的函数,比较函数 API 没准儿能帮我理解两个库做的不同权衡。文章写不下去,正好换换脑子。
Underscore 文档 里是这么描述 range 1 的:
.range([start], stop, [step])此函数用于创建 灵活有序 的整数列表,供
each和map循环。start 如果省略,默认为 0;step 默认为 1。
返回从 start 到(不含)stop 的整数列表,以 step 递增(或递减)。
注意,stop 小于 start 会返回空数组 —— 要是想倒着排,还得传负数 step。
示例:
_.range(10); //=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] _.range(1, 11); //=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] _.range(0, 30, 5); //=> [0, 5, 10, 15, 20, 25] _.range(0, -10, -1); //=> [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] _.range(0); //=> []
Ramda 里 对等的函数 是这个:
R.range :: Number -> Number -> [Number]返回一个从
from到(不含)to的数字列表。示例:
range(1, 5); //=> [1, 2, 3, 4] range(50, 53); //=> [50, 51, 52]
灵活度
Underscore 的函数明显更灵活。列表能正向也能反向,step 的值都能传不同的。
Ramda 出的晚,为何没有 Underscore 那种灵活度?请看三个充分理由:
-
没看过。既然 range 函数是刚需,我就得写一个。高端的 API,往往只需要 ____ 的写法2,包容还是排他呢?思来想去,排他性的更好,能和
for循环合作愉快。Python 的range()都知道吧,看 文档 就确定,我要写成排他性的。Underscore 我压根儿没看。 -
用不着。YAGNI3 原则很强大。
[20, 18, 16, 14, 12, 10, 8]这种需求的列表,我反正没遇到过。就因为万一哪天有人用得上,去提升灵活度,等于踏上不归路。你想想,for循环写成for var i in [1 .. 10] {/* ... */}会简单多少?你又会因为没用上当前丑陋的for循环语法提供的灵活而叹息几次?我一年一两次吧。不过无所谓,罕见情况总能应对。 -
太复杂。文章最开始的问题,当然和 Underscore 的函数有关。因为函数够复杂才能应对罕见情况,导致每次用之前基本上都得先看文档。Ramda 一切从简,一个函数只做一件事。
简洁 vs 费解
R.range() 的用法,聪明的你一想便知。边缘情况(传非整数将如何?to 小于 from 又将如何?)别去想。传一个初始值和一个比结束值大 1 的值就行:R.range(1, 4); //=> [1, 2, 3]。
再看 Underscore,也是这样调用 _.range(1, 4); //=> [1, 2, 3]。它还能创建反向列表。那要是想创建反向列表,是不是得 _.range(4, 1) 这样?不行,因为结果含第一参数,不含第二参数。想从 3 开始,_.range(3, 2) 又会直接停在结束值之前。它得对称,得刚好停在,正向列表例子传的第一参数 1 那儿。那这样 _.range(3, 0) 能行吧?
也不行。
看定义吧,那句提示最重要:
注意,stop 小于 start 会返回空数组 —— 要是想倒着排,还得传负数 step。
所以,创建递增列表,可以不传 step,但是递减的得传,即便以 1 递减。
现在再看文档里关键的一句。怪哉,给人家放到第三句:
返回从 start 到(不含)stop 的整数列表,以 step 递增(或递减)。
屏幕前的你,能知道「不含」stop,是因为读过 Ramda 文档,或者看过文中示例,亦或诸葛再世。尽管 API 本身没问题,是文档的问题,可二者关系密切。简单的函数,其文档往往也简洁明了,复杂的则冗长费解。
依我看,Underscore 最复杂的就是参数,群魔乱舞,一会儿 可选,一会儿 必传,过会儿又 可选。Ramda 不搞 可选参数,稳如泰山的参数排前面,变化多端的放最后,通过简单的柯里化,用旧函数构建新函数会很容易。可以从 step 入手,代入 1 构建 range 写出 steppedRange 函数。或者想做成递减的,简单实现一下,文档再一改,齐活儿。很容易就更新成了 R.range(3, 0); //=> [3, 2, 1],
可是真没必要。就像 Rich Hickey 讲的,保持简单更重要。
如果真的需要呢?
看到这里,你一定也会觉得 Ramda API 比 Underscore 的简单。你可能想问,我要是真的需要 Underscore 里某些特性呢?实用函数库不是应该考虑到尽可能多的场景吗?
我的回答是,一个好的 API 应该让常见的场景的变得容易,不常见的场景变得可能。
Ramda 不提供 Underscore 的倒着排 _.range(3, 0, -1); //=> [3, 2, 1],它是围绕着组合小函数的概念构建的。两个函数的简单组合就能完成:
R.reverse(R.range(1, 4)); //=> [3, 2, 1]
能接受吧。
如果需要跨着排呢?Underscore 又出招 _.range(10, 20, 2); //=> [10, 12, 14, 16, 18],Ramda 如何接招?
看招:
var evens = R.reject(R.modulo(R.__, 2));
evens(R.range(10, 20)); //=> [10, 12, 14, 16, 18]
如果想更通用,想随便传 start,stop 和 step 呢?
这样就行了:
R.reject(R.pipe(R.add(R.modulo(-start, step)), R.modulo(R.__, step)))(R.range(start, stop));
我这个,只是 Underscore API 的大体实现,完全实现就别了,我懒得写。(既然有了处理正数 step,剩下就的留给读者作为练习。)一个系统如果需要这种灵活度应当明确地指出,并且专函数专办。谁也不愿意搞乱代码库。但并不是说让你的每个函数都成 Ramda 里的那样。Ramda 是为了让你自己动手容易,但自己的 API 过于倾向简单可就错了。