還記得之前我們建立 Effect 時還有分 sync 跟 promise 的嗎?另外還有提到 Effect 像是藍圖,設計好程式的流程後我們才開始執行,這篇裡我們就要來介紹, Effect 中的 concurrency
接下來你可能會看到同樣的 function
fetchData
等等,被用在 promise 或是 Effect 中,Effect 中除了在第 3 篇中介紹的建立 Effect 的 function 外,大多不支援傳入 promise ,請假設這些在 Effect 中的 function 都已經包裝過,變成回傳 Effect 了
Effect.all
: 給 Effect 的更強大的 Promise.all
在前面提到說,我們不該在 Effect.gen
中使用像 for
這種固定次數的迴圈嗎?那是因為若使用了,你就沒辦法很好的發揮 Effect 的強大的 concurrency 的控制能力了,在使用 Promise 時,若可以我們也會避免像這樣的 code
const result = []
for (const id of list) {
const data = await fetchData(id)
result.push(data)
}
這麼做會導致我們一次只能執行一個 fetchData
,需要等到一個 fetchData
結束才能執行下一個,那該怎麼辦,此時我們會像這樣寫
const result = await Promise.all(list.map(id => fetchData(id)))
透過 Promise.all
可以幫助我們將 fetchData
的處理平行化,這可能會幫助我們的程式更快取得需要的資料,但如果今天突然有個需求,你需要控制你的程式,一次最多不要發出超過 3 條連線呢?這時有個很好用的小套件叫 p-map
import pMap from 'p-map'
const result = await pMap(list, (id) => fetchData(id), { concurrency: 2 })
而在 Effect 我們可以像這樣做
const resultEffect = Effect.all(list.map((id) => fetchData(id)), { concurrency: 2 })
Effect.all
就像 Promise.all
一樣,可以幫助我們平行執行 Effect ,而且還已經自帶 concurrency
的控制了,不過不太一樣的是,預設的 Effect.all
也是一個接著一個執行的,也就是預設的 concurrency
值是 1
const resultEffect = Effect.all(list.map((id) => fetchData(id))) // 這樣會一個接著一個執行
如果要像 Promise.all
的效果,你需要將 concurrency
設為 unbounded
,也就是無限制的
const resultEffect = Effect.all(list.map((id) => fetchData(id)), { concurrency: 'unbounded' })
另外如果你是用在 pipe
中,因為 Effect.all
的輸入很複雜,難以同時做成 data-first 與 data-last 的版本,因此 Effect 提供了另一個 Effect.allWith
讓你用在 pipe 中
const resultEffect = pipe(
list.map((id) => fetchData(id)),
Effect.allWith({ concurrency: 'unbounded' }),
)
到這邊我們已經對於怎麼平行執行 Effect 有基本的認識,至於為什麼 Effect.all
的輸入會複雜呢,接下來我們就會提到更多 Effect.all
的神奇用法
Effect.all
平行執行 object 的 values平常我們使用 Promise.all
跟上面的 Effect.all
都是回傳陣列,不知道你有沒有碰過你有一個 object ,裡面有多個東西需要同步執行的情況呢?假如我們現在在做一個手機上的 widget ,是同時顯示時間跟天氣的,而這兩個東西都需要 async 的 API 來取得,你可能會像這樣寫
const weather = await fetchWeather()
const time = await fetchTime()
但這樣寫就沒有平行執行了,感覺速度好像慢了一點,於是你改用 Promise.all
// 加上 as const ,讓 TypeScript 可以推導成更準確的 tuple 型態
const [weather, time] = await Promise.all([fetchWeather(), fetchTime()] as const)
而你想要把它包成一個 function 叫 fetchWidgetData
,考量到回傳 tuple 可能會讓人忘記到底是哪一筆資料在前,於是你希望回傳值為 { weather, time }
function fetchWidgetData() {
const [weather, time] = await Promise.all([fetchWeather(), fetchTime()] as const)
return {
weather,
time,
}
}
而這在 Effect 中可以這樣寫
function fetchWidgetData() {
return Effect.all({
weather: fetchWeather(),
time: fetchTime(),
})
}
Effect.all
會自動的處理每個 object 的 value ,等待 Effect 完成後,再包成 object 回傳
延續上一個 widget 抓資料的範例,假設在 widget 中,我們抓取資料有可能會因為網路不穩而出錯,可能兩個都發生錯誤,或是只有一個,這時我們不會希望整個程式因此中止,因此,我們要為沒成功抓取的資料提供一個錯誤訊息顯示
回到 promise 的範例,假設今天的天氣 API 無法使用,抓取時一定會發生錯誤
function fetchWidgetData() {
const [weather, time] = await Promise.all([fetchWeather(), fetchTime()] as const)
return {
weather,
time,
}
}
在上面的例子中,我們碰到了個問題
Promise.all
只要碰到一個錯誤就會全部中止,也就是fetchTime
的結果你也拿不到,只會獲得一個錯誤,並且無法判斷是哪個 promise 產生的錯誤
於是我們要換成 Promise.allSettled
function fetchWidgetData() {
const [weather, time] = await Promise.allSettled([fetchWeather(), fetchTime()] as const)
return {
weather: weather.status === 'fulfilled' ? weather.value : 'Fail to get current weather',
time: time.status === 'fulfilled' ? time.value : 'Fail to get current time',
}
}
那 Effect.all
又要怎麼解決這種情況呢?首先 Effect.all
預設也跟 Promise.all
一樣,只要有一個出現錯誤,就會全部停止,並回傳錯誤,那該怎麼辦?我們要改變 Effect.all
的模式 (mode
)
import { Effect, Either } from 'effect' // 需要額外引入 Either
function fetchWidgetData() {
return pipe(
Effect.all(
{
weather: fetchWeather(),
time: fetchTime(),
},
{ mode: 'either' },
),
Effect.map(({ weather, time }) => ({
weather: Either.getOrElse(weather, () => 'Fail to fetch weather'),
time: Either.getOrElse(time, () => 'Fail to fetch time'),
})
)
}
改成 either
模式後,就不會在第一個錯誤終止了,相對的,會把所有的結果用一種叫 Either
的資料型態包裝傳回來,這種型態可以代表可能成功或是失敗,我們透過 Either.getOrElse
取得成功的值,並提供失敗時的錯誤訊息
這次一次介紹了比較多的 concurrency 相關的操作,希望有讓你體驗到 Effect 的強大,下一篇要來介紹 Effect 中的錯誤,與如何處理錯誤