在 for
Loop 內呼叫 Asynchronous Function 為常見需求,Imperative 做法是在 for of
內搭配 await
與 Side Effect 完成,但在不使用 Side Effect 的 FP 該如何實現呢 ?
Version
macOS Mojave 10.14.6
VS Code 1.37.1
Quokka 1.0.240
ECMAScript 2017
Ramda 0.26.1
Wink-fp 0.0.6
Imperative
let inc = async x => x + 1;
let data = [1, 2, 3];
let fn = async arr => {
let sum = 0;
for(let x of arr)
sum = sum + await inc(x);
return sum;
};
await fn(data); // ?
第 1 行
let inc = async x => x + 1;
ES2017 引進了 async await
,只要在 function 名稱前加上 async
,則回傳為 Promise
,而非單純 int。
第 8 行
for(let x of arr)
sum = sum + await inc(x);
使用 for of
loop 調用 inc()
累加至 sum
。
因為 inc()
為 asynchronous function 回傳 Promise
,為了等 inc()
回傳值後才繼續執行,特別加上了 await()
。
第 5 行
let fn = async arr => {
...
};
因為 fn()
內使用了 await()
,ES6 語法規定在 function 前面要宣告為 async
成為 asynchronous function,回傳 Promise
。
14 行
await fn(data); // ?
因為 fn()
回傳為 Promise
,而非單純 int,因此要加上 await
等 Promise
回傳資料。
儘管因為 inc()
宣告為 async
, fn()
也成為 asynchronous function,但整體思維與寫法還是與 imperative 很像,這就是 async await
的魅力。
No Side Effect
let inc = async x => x + 1;
let data = [1, 2, 3];
let fn = async arr => {
let arr_ = await Promise.all(arr.map(x => inc(x)));
return arr_.reduce((a, x) => a + x);
};
await fn(data); // ?
async await
需搭配 sum
使用 side effect,是否有更 functional 方式呢 ?
- 使用
map()
對 array 內所有 element 執行inc()
,由於inc()
回傳為Promise
,因此map()
回傳為 Promise array - 使用
Promise.all()
對 Promise array 合併成單一 Promise - 使用
await
等待合併後的單一 Promise,arr_
為真正 array,進入 synchronous 階段 - 使用
reduce()
求和,無需sum
做 side effect
then()
let inc = async x => x + 1;
let data = [1, 2, 3];
let fn = arr =>
Promise.all(arr.map(x => inc(x)))
.then(x => x.reduce((a, x) => a + x));
await fn(data); // ?
可使用 then()
取代 await
,如此不需要中繼變數,也不需將 fn()
宣告為 async
,如此更具 FP 風格。
Point-free
import { reduce, map } from 'ramda';
let inc = async x => x + 1;
let data = [1, 2, 3];
let fn = arr =>
Promise.all(map(inc, arr))
.then(reduce((a, x) => a + x, 0));
await fn(data); // ?
雖然將 await
拿掉後更有 functional 風格,可再使用 Ramda 的 map()
與 reduce()
,將 Promise.all()
與 then()
的 callback 給 point-free。
sum()
import { sum, map } from 'ramda';
let inc = async x => x + 1;
let data = [1, 2, 3];
let fn = arr => Promise.all(map(inc, arr)).then(sum);
await fn(data); // ?
事實上 Ramda 已經提供了 sum()
,專門負責求和,因此可使用 sum()
取代 reduce()
。
可以發現 fn()
已經簡化到只剩一行。
pipe()
import { sum, map, pipe, then } from 'ramda';
let inc = async x => x + 1;
let promiseAll = ps => Promise.all(ps)
let data = [1, 2, 3];
let fn = pipe(
map(inc),
promiseAll,
then(sum)
);
await fn(data); // ?
其實原本做法已經有 pipeline 概念,但卡在 Promise
原生的 Promise.all()
與 then()
不夠好用,因此串不起來。
Ramda 有提供 function 版本的 then()
可直接使用。
Promise.all()
則可自己包成 function 後,就能使用 pipe()
了。
Wink-fp
import { sum, map, pipe, then } from 'ramda';
import { promiseAll } from 'wink-fp';
let inc = async x => x + 1;
let data = [1, 2, 3];
let fn = pipe(
map(inc),
promiseAll,
then(sum)
);
await fn(data); // ?
像 promiseAll()
這種小 function 非常實用, Wink-fp 已經提供可直接使用。
Function Composition
import { sum, map, compose, then } from 'ramda';
import { promiseAll } from 'wink-fp';
let inc = async x => x + 1;
let data = [1, 2, 3];
let fn = compose(
then(sum),
promiseAll,
map(inc)
);
await fn(data); // ?
既然能使用 pipe()
達成 pipeline,反向使用 compose()
就是 function composition 了。
Conclusion
- 只要是用到 Promise 的 function,就是 asynchronous function,並不是有
async
keyword 才是,那只是給 compiler 看的 async await
是 imperative 產物,接近傳統思考習慣,會伴隨大量中繼變數與 side effect 使用then()
則是 FP 產物,會自然使用 pure function,不會使用 side effect,但原生的then()
是掛在 Promise 下,不便於 pipeline,可改用 Ramda 的then()
Promise.all()
也是相同道理,因此在 Wink-fp 提供promiseAll()
- 個人較偏好
then()
,但有時真的寫不出來時,async await
也是可行,實務上建議then()
用在 data 處理,就當成map()
使用,而async await
用在處理 side effect
Reference
MDN, Promise
MDN, Promise.all()
Ramda, map()
Ramda, reduce()
Ramda, sum()
Ramda, pipe()