在上一篇,我們一起做了一個 pipeline,把一連串應收金額的產出步驟用 pipe
串起來。pipe
雖然很適合銜接簡單流程,但當需求變得複雜、需要插入條件、早退或錯誤處理時,就容易寫成巢狀的 pipe + callback
樣式:括號變多、程式一路往右飄,中間值在閉包裡穿梭,閱讀與維護成本直線上升。這一篇要示範如何把這種巢狀地獄的 pipeline
改用 Effect.gen
重構。流程中會用 yield*
逐步取值並命名,並把巢狀流程「攤平」成敘事式的可讀樣式。你可以把它想成類似 async/await
的寫法。Effect 在設計 Effect.gen
時,就是用類似 async/await
的寫法來設計的。
所謂「巢狀 pipe + callback」是指以 pipe
/andThen
(或類似方法)連續串接步驟,並以匿名函式承接每一步的輸出,例如:a.pipe(andThen(x => b(x).pipe(andThen(y => c(y)))))
。這種寫法把中間值藏在多層閉包裡;隨著需求增加,括號與縮排會急速加深。一旦插入條件判斷、早退或錯誤處理,閱讀與重構成本就會直線上升。
import { Console, Effect } from "effect"
/**
* 以 Effect 形式取得目前時間(毫秒)。
* @returns Effect<number, never> 代表目前時間戳(ms)的純同步 Effect
*/
const now = Effect.sync(() => Date.now())
/**
* 簡單錯誤類別:執行超過門檻時拋出。
*/
class TooSlowError extends Error {
readonly name = "TooSlowError"
constructor(readonly elapsedMs: number, readonly thresholdMs: number) {
super(`Too slow: ${elapsedMs}ms (> ${thresholdMs}ms)`)
}
}
/**
* 為傳入的 Effect 加上執行時間量測與日誌,並支援簡單錯誤分流。
*
* 行為:
* - 執行前記錄起始時間。
* - 先印出 "start running..."。
* - 成功後印出耗時(毫秒)。
* - 若提供 thresholdMs 且耗時超過門檻,回傳 TooSlowError(錯誤分流)。
* - 原本的失敗錯誤不改變,直接向外拋出(維持語意)。
*
* @template A 成功值型別(與原 Effect 相同)
* @template E 失敗錯誤型別(與原 Effect 相同)
* @template R 需求環境(與原 Effect 相同)
* @param self 要被量測與記錄日誌的 Effect
* @param thresholdMs 門檻(毫秒);超過則以 TooSlowError 失敗
* @returns Effect<A, E | TooSlowError, R>
*/
const elapsed = <A, E, R>(
self: Effect.Effect<A, E, R>,
thresholdMs?: number
): Effect.Effect<A, E | TooSlowError, R> =>
now.pipe(
Effect.andThen((start) =>
Console.log("start running...").pipe(
Effect.andThen(() => self),
// 注意:這裡只處理成功分支;若 self 失敗,錯誤會直接外拋(維持語意)。
Effect.andThen((value) =>
now.pipe(
Effect.andThen((end) =>
Console.log(`elapsed: ${end - start}ms`).pipe(
Effect.andThen(() =>
thresholdMs !== undefined && end - start > thresholdMs
? Effect.fail(new TooSlowError(end - start, thresholdMs))
: Effect.succeed(value)
)
)
)
)
)
)
)
)
流程:
start
。self
。end - start
(毫秒)。end - start > thresholdMs
,直接以 TooSlowError
早退(錯誤分流)。在這種巢狀寫法裡,讀者必須在多層閉包間來回追蹤上下文。start
、end
這類中間值仰賴函式巢狀來維持作用域並跨層傳遞;一旦插入條件判斷、早退或錯誤處理,巢狀會迅速加深,閱讀與維護成本直線上升。
Effect.gen
改寫(更可讀、更好維護)const elapsedGen = <A, E, R>(
self: Effect.Effect<A, E, R>,
thresholdMs?: number
): Effect.Effect<A, E | TooSlowError, R> =>
Effect.gen(function*() {
const start = yield* now
yield* Console.log("start running...")
const value: A = yield* self
const end = yield* now
const elapsedMs = end - start
yield* Console.log(`elapsed: ${elapsedMs}ms`)
// 早退:若超過門檻,直接以 TooSlowError 失敗返回(錯誤分流)。
if (thresholdMs !== undefined && elapsedMs > thresholdMs) {
return yield* Effect.fail(new TooSlowError(elapsedMs, thresholdMs))
}
return value
})
// 成功案例:不超過門檻(或不設定門檻)
Effect.all([
elapsed(Effect.succeed("OK"), 1000),
elapsedGen(Effect.succeed("OK"), 1000)
]).pipe(
Effect.matchEffect({
onSuccess: ([a, b]) => Console.log(`success: ${a}, ${b}`),
onFailure: (err) => Console.log(String(err))
}),
Effect.runSync // 輸出:success: OK, OK
)
// 錯誤案例:超過門檻 → TooSlowError
function busyWait(ms: number): void {
const start = Date.now()
while (Date.now() - start < ms) void 0
}
const slowSelf = Effect.sync(() => {
busyWait(30)
return 42
})
Effect.all([
elapsed(slowSelf, 5),
elapsedGen(slowSelf, 5)
]).pipe(
Effect.matchEffect({
onSuccess: () => Console.log("unexpected success"),
onFailure: (err) => Console.log(String(err))
}),
Effect.runSync // 輸出:TooSlowError: Too slow: 30ms (> 5ms)
)
在以下情境,優先使用 Effect.gen
:
需要上一個步驟的值(跨多步驟傳遞/命名中間值)。
有條件分支、早退、錯誤分流或需要 try/catch(例如 catchAll
)。
想以「逐行敘事」插入/重排步驟,維持高可讀性。
需要在同一作用域整合多個結果再計算。
在以下情境,改用 andThen
(含 tap
、map
):
pipe
內更精簡。混用建議:
gen
管控分支與早退;在 gen
內用 tap
/tapError
放置不改變值的副作用。pipe + andThen
組裝,再在需要的地方以 gen
取值。const elapsedDo = <A, E, R>(
self: Effect.Effect<A, E, R>,
thresholdMs?: number
): Effect.Effect<A, E | TooSlowError, R> =>
Effect.Do.pipe(
Effect.bind("start", () => now),
Effect.tap(() => Console.log("start running...")),
Effect.bind("result", () => self),
Effect.bind("end", () => now),
Effect.tap(({ end, start }) => Console.log(`elapsed: ${end - start}ms`)),
Effect.flatMap(({ end, result, start }) =>
thresholdMs !== undefined && end - start > thresholdMs
? Effect.fail(new TooSlowError(end - start, thresholdMs))
: Effect.succeed(result)
)
)
Do
可以逐步綁定命名欄位,最後再 map 出結果。Effect.gen
通常更直覺。Effect.gen
。