點燈坊

失くすものさえない今が強くなるチャンスよ

如何以 FP 在 For Loop 內呼叫 Asynchronous Function ?

Sam Xiao's Avatar 2019-09-12

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,因此要加上 awaitPromise 回傳資料。

async000

儘管因為 inc() 宣告為 asyncfn() 也成為 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

async001

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 風格。

async002

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。

async003

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() 已經簡化到只剩一行。

async004

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() 了。

async005

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 已經提供可直接使用。

async006

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 了。

async007

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()