Ramda — 自动柯里、data-last 的实用函数式工具库
已复核Ramda 是一个面向实用函数式编程的 JavaScript 工具库。日常类比:流水线上先装好“怎么处理”,最后才放原料;每个工位默认复印一份再改,不在原件上涂。
你写:
import * as R from 'ramda';
const names = R.map(R.prop('name'));names([{ name: 'Ada' }, { name: 'Bob' }]);R.map 和 R.prop 都已柯里化,数据参数在最后。先拿到 names,再喂数组。固定 0.32.0 的 source/index.js 导出 272 个公开符号,package.json 声明 sideEffects: false。
不理解 Ramda 的柯里、占位符和“复制路径再写入”,就解释不了下面几件事:
- 为什么
R.map(fn)可以先当转换器保存,稍后再给列表 - 为什么
R.pipe的结果不会自动再柯里一次 - 为什么
R.assoc('c', 3, obj)不会改obj - 为什么带默认参数的函数丢给
R.curry会在中途炸掉
固定 0.32.0 的主链可以拆成五步:
-
按函数拆源码再组装导出:作者源在
source/;require走构建后的src/index.js,import走es/index.js。条件 exports 同时暴露./src/*、./es/*、./dist/*。 -
自动柯里:多数函数经
_curry1/_curry2/_curry3或curryN包装。R.curry(fn)用fn.length当 arity;一次可以喂多个实参,不必只喂一个。 -
占位符补洞:
R.__是{ '@@functional/placeholder': true }。_curryN遇到占位符会留下空位,等后续调用填上,再决定是执行还是继续返回柯里函数。 -
管道不自动柯里:
R.pipe要求至少一个函数,第一函数可多元,其余必须一元;结果只保证 arity 等于第一个函数的length。R.compose是pipe.apply(this, reverse(arguments))。R.flow(自 0.30.0)则是seed + 函数数组的立即求值管道。 -
写入先复制路径:
R.assoc转到assocPath;沿路径浅拷贝,非原语仍按引用挂上。R.map经_dispatchable分发到fantasy-land/map/map、transducer 或默认数组/对象实现,默认实现写新结构。
案例 1:先拼转换器,最后才给数据
Section titled “案例 1:先拼转换器,最后才给数据”const rename = R.map(R.prop('name'));rename([{ name: 'Ada' }, { name: 'Bob' }]); // => ['Ada', 'Bob']
R.map(R.prop('name'), [{ name: 'Ada' }]); // 一次喂完也可以map 是 _curry2。只给 iteratee 时返回新函数;再给 list 才遍历。对象会走 keys 后写入新对象,不会改原对象。
案例 2:R.__ 把中间参数留空
Section titled “案例 2:R.__ 把中间参数留空”const greet = R.replace('{name}', R.__, 'Hello, {name}!');greet('Alice'); // => 'Hello, Alice!'replace 是三元柯里。第二次参数用占位符时,返回的函数只缺那一个洞。_isPlaceholder 认的是 @@functional/placeholder === true,不是任意假值。
案例 3:assocPath 复制路径,不改原对象
Section titled “案例 3:assocPath 复制路径,不改原对象”const user = { a: { b: { c: 0 } } };const next = R.assocPath(['a', 'b', 'c'], 42, user);// next.a.b.c === 42// user.a.b.c === 0路径上缺失或非对象的节点会被换成 [] 或 {}。注释写明:原型属性会被摊到新对象上,非原语按引用复制。这是浅路径拷贝,不是整树 clone。
-
以为
R.pipe/R.compose会自动柯里:源码写明结果不自动柯里。pipe(f, g, h)(a)(b)的符号展开是h(g(f(a)))(b),不是h(g(f(a, b)))。 -
把默认参数函数丢给
R.curry:curry用fn.length。默认参数不计入 length,于是curry((a, b, c = 2) => a + b + c)会在第三次调用时把数字当函数执行。 -
把
R.assoc当成深拷贝:它只复制到写入路径;兄弟分支仍共享引用。要断开整棵树需R.clone,且函数仍按引用复制。 -
以为
R.map只处理数组:对象走keys;函数会被组合成新函数;带map/fantasy-land/map的 functor 会 dispatch;transformer 则进入 transducer。 -
把“从不改用户数据”理解成运行时强制:设计目标是尽量无副作用,库并不拦截你传入的可变对象在回调里被改写。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 想先拼 data-last 管道,再在调用处喂数据
- 需要默认不可变的对象/列表更新,并能接受路径拷贝语义
- 打包器能消费
sideEffects: false的 ESM / CJS 条件导出
不适用:
- 团队习惯
_.set/_.assign原地更新——先看 lodash 默认构建,不要只改 import 名 - 只要防抖节流、模板字符串这类宿主副作用工具——Ramda 核心不提供与
_.debounce对位的实现 - 需要 draft 赋值语法——immer 更贴近
- 不能接受
R.curry依赖function.length的边界
固定版本边界
Section titled “固定版本边界”- 本文绑定
ramda/ramda@f0b1fb524a681bc8c37dd6c35886420f8c2470c3,tagv0.32.0与 npmramda@0.32.0的gitHead指向同一提交。 package.json无运行时依赖;exports区分require→./src/index.js与import→./es/index.js。source/index.js在该提交有 272 条export { default as ... }。R.flow自 v0.30.0 加入,语义是reduce(applyTo, seed, pipeline)。- 本文未安装依赖、运行上游测试或测量 bundle,状态保持
UNVERIFIED。
- data-last 是为了柯里,不是为了好看——先固定变换,再接数据,才能把
R.map(R.prop('name'))存成值。 - 管道和柯里是两层合同——
pipe/compose不自动柯里;要部分应用请用已经柯里过的函数,或显式curry。 - 不可变更新通常只复制路径——
assocPath共享未触及的子树;循环引用与函数复制边界由clone另说。 - dispatch 让同一函数服务数组、对象、functor 和 transducer——读
_dispatchable比背 API 列表更重要。
R.pipe(Math.pow, R.negate)的返回值,能像R.map那样先只喂一个参数再喂第二个吗?R.assoc('c', 3, obj)会不会改obj本身?R.curry((a, b, c = 2) => a + b + c)(1)(2)接下来再喂7,会得到10吗?
检查点:
- 不能按柯里函数理解。
pipe明确不自动柯里,arity 等于第一个函数的length。 - 不会。
assoc返回新对象,原对象保持原值。 - 不会稳定得到
10。默认参数使fn.length变小,第三次调用可能把数字当函数执行。
- 文档:ramdajs.com
- 固定源码:ramda/ramda —— 本文绑定提交
f0b1fb524a681bc8c37dd6c35886420f8c2470c3 - lodash —— 默认构建的 mutate / iteratee-first 对照组
- immer —— 另一条不可变更新路线:draft 赋值而不是 data-last 管道