纯手写函数式很麻烦,要照顾到柯里化,要纯,要有容器等等。
因此市面上出现了函数式的库,比如 lodash 里面有很多实用函数,ramda.js 也是属于这类,最近开始学这个类库了,用它来控制 Promise 的体验非常棒,因此计划写一下这篇文章总结总结。
# Ramda ↵
目前已经存在许多优秀的函数式的库。通常它们作为通用工具包,可以用于多种编程范式。Ramda 的目标更为专注:专门为函数式编程风格而设计,更容易创建函数式 pipeline、且从不改变用户已有数据。
摘抄自 Ramda 中文网
英文官网
以下是一个简单的例子,来体会一下 ramda:
00const _ = require('ramda') 01 02let a = [1, 2, 3, 4]; 03 04let double = _.map(e => e * 2); 05 06let b = double(a); 07// => 08// [2, 4, 6, 8]
这与下面的这种写法是等价的:
00let a = [1, 2, 3, 4]; 01let b = a.map(e => e * 2); 02// => 03// [2, 4, 6, 8]
# 基本概念 ↵
函数式里的函数其实更像数学里的那种函数。
# 纯的
以下的函数
b
并不纯,因为它引用了外部变量,函数的输出结果依赖于外部环境了,而不是给定值,结果一定不变。00let a = 1; 01 02let b = () => a + 1; 03 04b(); 05// => 06// 2
以下是纯的写法:
00let a = 1; 01 02let b = a => a + 1; 03 04b(a); 05// => 06// 2
只要
b
的参数确定了,其返回值就是那样,不会改变。# 柯里化
函数的参数一个一个的给进去,函数的运行求值,其实就是消元的过程,最终得到值。
z = f(x, y)
z’ = f(a, y)
val = f(a, b)
(x,y 是变量;a,b 是常数;求值 val 的过程就是消元的过程)
观察如下例子:
00let add = a => b => a + b; 01 02add(1)(2); 03// => 04// 3 05 06let addOne = add(1); 07// => 08// 得到一个 Function 09 10addOne(6); 11// => 12// 7
函数 add 是一个柯里化的函数,对它消参的过程就是求值的过程。
在 ramda 里可以调用如下函数来将一个普通函数变成柯里化的函数:
00 01let _add = (a, b) => a + b; 02 03let add = _.curry(_add); 04 05add(1)(2); 06// => 07// 3 08 09let addOne = _add(1); 10addOne(3); 11// => 12// 4 13 14// 详见 http://ramda.cn/docs/#curry
# _.map ↵
ramda 具有很强的表现力,本文一开始的时候,写过一个 map 的例子:
00const _ = require('ramda') 01 02let a = [1, 2, 3, 4]; 03 04let double = _.map(e => e * 2); 05 06let b = double(a); 07// => 08// [2, 4, 6, 8]
这段代码将 a 里的全体变成原来的两倍,然后保存在 b 里。
# _.pipe ↵
ramda 的管道可以将一个个函数拼接成更长的函数。
00let a = [1, 2, 3, 4]; 01 02let addOne = a => a + 1; 03 04let addOneForArray = _.map(addOne); 05 06let addTwoForArray = _.pipe( 07 addOneForArray, 08 addOneForArray 09); 10 11addOneForArray(a); 12// => 13// [2, 3, 4, 5] 14 15addTwoForArray(a); 16// => 17// [3, 4, 5, 6] 18 19addOneForArray(addOneForArray(a)); 20// => 21// [3, 4, 5, 6]
# _.pipeP ↵
这跟上面的管道一样,不过这个管道是针对 Promise 的:
00let test = _.pipeP( 01 a => Promise.resolve(a + 1), 02 b => { 03 console.log(b); 04 // => 05 // 2 06 return Promise.resolve(b); 07 } 08); 09 10test(1);
pipeP 在内部执行的时候会自动帮你 then 好。
# 一个例子 ↵
用 ramda 重写 《函数式的 Promise 对异步的抽象》 里的并发下载。
00// download.js 01const rp = require('request-promise') 02 , fs = require('then-fs') 03 , url = require('url') 04 , path = require('path') 05 , STORE_TO = __dirname 06 , FILE_BASE = path.join(STORE_TO, 'd') 07 08// 下载一个文件 09function download(raw_url){ 10 const fileUrl = url.parse(raw_url) 11 , filePath = path.parse(fileUrl.path) 12 , fileName = filePath.base 13 , fileLocation = path.join(FILE_BASE, fileName) 14 , target = fs.createWriteStream(fileLocation) 15 16 console.log(fileLocation); 17 18 return new Promise((res, rej) => { 19 // Promise Resolved When Success 20 target.on('close', res); 21 // Promise Rejected When Error 22 target.on('error', rej); 23 // Start Downloading 24 rp.get(raw_url).pipe(target); 25 }); 26} 27 28module.exports = download;
download.js 用于完成单个文件的下载,并且返回 Promise
00// main.js 01const _ = require('ramda') 02 , download = require('./download') 03 , PromiseAll = Promise.all.bind(Promise) 04 05// 图片数组 06let urls = [ 07 'http://source.ijarvis.cn/blog/test/a.jpg', 08 'http://source.ijarvis.cn/blog/test/b.png', 09 'http://source.ijarvis.cn/blog/test/c.jpg', 10 'http://source.ijarvis.cn/blog/test/r.png', 11 'http://source.ijarvis.cn/blog/test/s.jpg', 12 'http://source.ijarvis.cn/blog/test/t.png', 13 'http://source.ijarvis.cn/blog/test/v.jpg', 14 'http://source.ijarvis.cn/blog/test/y.jpg', 15 'http://source.ijarvis.cn/blog/test/65912770_p2.png' 16]; 17 18let dlOneByOne = _.pipeP( 19 // Map 到 Promise 20 _.map(download), 21 PromiseAll 22); 23 24dlOneByOne(urls);