2018-01-06
fp
Ramda 里的 Promise
纯手写函数式很麻烦,要照顾到柯里化,要纯,要有容器等等。
因此市面上出现了函数式的库,比如 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);




回到顶部