iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

還記得之前我們建立 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 中的錯誤,與如何處理錯誤

Reference


上一篇
4. Effect 的基本使用
下一篇
6. Effect 中的錯誤
系列文
Effect 魔法:打造堅不可摧的應用程式7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言