Software Development2025 IT 鐵人 30 天挑戰
Effect 中的 Data Types(一)
Effect Type vs Data Type
- Effect Type:一個「尚未執行的工作描述」。它可能需要外部環境才能跑,實際執行時要嘛成功回傳結果、要嘛以錯誤結束。在你真的啟動它之前,它不會做任何事也不會產生副作用。
- Data Type:描述「長什麼樣、有哪些可能狀態」的純資料值(在 TS 中建立/操作即時且無副作用)。
為何需要 Data Types?
- 明確語意
- 用型別把可能狀態顯性化:Option(可能沒有值)、Either/Result(成功或帶型別的錯誤)、Exit(程序結果)、Cause(錯誤結構與脈絡)。
- 好處:編譯期就能強迫你處理所有分支,減少執行期例外與 if-null 風險。
- 邊界清晰、互通良好
- 在純同步邏輯裡,用 Option/Either 等 Data Types 安全地建模資料與分支。
- 當進入「需要副作用/非同步/資源/錯誤通道」的情境,再把這些資料型別嵌入到 Effect 的成功或錯誤通道中(例如
Effect<R, E, A>的 E 或 A)。 - 重點是分工:Data Types 負責描述狀態;Effect 負責描述執行流程與副作用。兩者組合,讓流程既可組合又可檢查。
- 不可變、可比對與高品質集合
- 使用不可變結構與結構性相等能避免共享可變狀態帶來的錯誤,利於快照/重試/快取。
接下來我們來介紹一些常用的 Data Types 吧~
data types 應用情境太多了。不過其實都很簡單,所以我們只會透過一些常見的例子來簡單介紹用法。如果你想要了解更多,可以參考官方文件。
Option:更安全的「可能沒有值」
- 用途
- 用型別安全方式表達「也許沒有值」:Some/None,避免 null/undefined 地雷。
- 適用情境
- 讀取可選設定、環境變數、URL 查詢參數。
- 查表或搜尋可能找不到的值。
- 純同步邏輯中想避免 try/catch 與例外的控制流。
常見操作
import { Option } from "effect";
const USER_ID = null
// 透過可能为 null/undefined 的值建立 Option
const maybeUserId = Option.fromNullable(USER_ID)
// 用 match 模式比對
const greeting = Option.match(maybeUserId, {
onSome: (id) => `Hi, ${id}`,
onNone: () => "Guest"
})
const userIdOrDefault = Option.getOrElse(maybeUserId, () => "unknown")- 說明
Option.fromNullable(x): 把null/undefined轉成Option,避免 if-null 判斷散落在程式中。Option.match(maybe, { onSome, onNone }): 明確處理兩種情況(有值/無值),讓控制流更清楚。Option.getOrElse(maybe, fallback): 真的需要「落地成值」時才提供預設值,平時維持Option以利後續組合。
依條件建立:liftPredicate
const isPositive = (n: number) => n > 0;
// 顯式用 some/none 建立
function parsePositiveExplicit(n: number): Option.Option<number> {
return isPositive(n) ? Option.some(n) : Option.none()
}
// 用 liftPredicate 更簡潔:回傳 (b: number) => Option<number>
const parsePositive = Option.liftPredicate(isPositive)- 說明
liftPredicate(pred)會回傳一個函式:給它一個值,若通過條件就回Some(value),否則回None。- 等價於手寫
pred(x) ? Option.some(x) : Option.none(),但更精煉且可重用。 - 適合用於輸入過濾、表單欄位驗證、URL 參數解析等情境,後續可直接用
map/flatMap/match串接。
建模可選屬性(key 永遠存在,value 可選)
- 鍵會一直存在(例如
email),但其值型別為Option<string>,表示可能沒有值。
interface User = {
readonly id: number;
readonly username: string;
readonly email: Option.Option<string>;
};補充:選擇 Option 而非email?: string的好處
- 鍵是否存在:
Option<T>下,key 固定存在(結構穩定);email?可能連鍵都缺席。 - 執行期語意:
Option以Some/None明確表示有值/無值,可被模式比對;email?僅以鍵缺席或undefined表達,語意容易分散。 - 可組合性:
Option具備map/flatMap/match/getOrElse等 API,能以表達式風格串接;email?常需要零散的if/?./??邏輯。 - 型別強迫處理:
Option迫使呼叫端顯式處理None,降低漏判空值的機率;email?容易在流程中被忽略。 - 內部資料模型 vs 對外介面: 內部使用
Option讓結構穩定、語意清楚;對外輸出(API/JSON)再轉成可選鍵或null。
Either:具名錯誤或兩路分支
-
用途
- 同時攜帶 Left(常作錯誤)與 Right(成功)。比 Option 更能描述「為何失敗」。
-
適用情境
- 同步的輸入驗證、解析(parse)與轉換,需要保留錯誤訊息或結構。
- 與非 Effect 區域互動,傳遞成功/失敗而不丟例外。
-
說明
Either適合作為「簡單可判別聯合」用於同步資料與錯誤建模;若要表達 Effect 的完整結果(成功、錯誤、缺陷、被中斷等),建議使用Exit(官方建議)。參考:Either 官方文件。
常見操作
import { Either } from "effect";
// 建立與基本使用
function parseIntegerEither(s: string) {
const n = Number(s)
if (!Number.isInteger(n)) {
return Either.left("not an int")
}
return Either.right(n)
}
const rightValue = Either.right(1)
const leftValue = Either.left("oops")
// 右偏映射(成功路)
const mapped = Either.map(rightValue, (n) => n + 1)
// 鏈接(成功路)
const chained = Either.flatMap(parseIntegerEither("42"), (n) => Either.right(n + 1))
// 轉換錯誤(左路)
const normalizedErr = Either.mapLeft(parseIntegerEither("x"), (e) => ({ message: e }))
// 明確處理兩個分支
const rendered = Either.match(parseIntegerEither("foo"), {
onRight: (n) => `ok: ${n}`,
onLeft: (err) => `error: ${err}`
})
// 輸出:
// rightValue: { tag: 'Right', value: 1 }
// leftValue: { tag: 'Left', error: 'oops' }
// mapped: { tag: 'Right', value: 2 }
// chained: { tag: 'Right', value: 43 }
// normalizedErr: { tag: 'Left', error: { message: 'not an int' } }
// rendered: error: not an int- 說明
Either.right/left:建立成功/失敗。Either.map / flatMap:右偏操作,僅在成功時變換或鏈接。Either.mapLeft:只在失敗分支變換錯誤型別/結構。Either.match:在一處完整處理兩個分支(成功/失敗)。
Exit:把執行結果資料化(測試、邊界、批次)
- 用途
- 描述 Effect 執行後的結果:
Exit.Success<A>或Exit.Failure<Cause<E>>。概念上近似Either<A, Cause<E>>。
- 描述 Effect 執行後的結果:
- 適用情境
- 在邊界與批次流程集中蒐集「成功/失敗」而不丟例外或 Promise reject。
- 測試/工作系統:一次跑多個 Effect,逐一分析、彙整結果。
常見操作
import { Effect, Exit, Cause } from "effect";
// 1) 同步執行並取得 Exit(成功)
const okExit = Effect.runSyncExit(Effect.succeed(42))
// 2) 同步執行並取得 Exit(失敗)
const errExit = Effect.runSyncExit(Effect.fail("my error"))
// 3) 匹配特定 Exit 並加以處理成功與失敗情境
// ┌─── string
// ▼
const renderedOk = Exit.match(okExit, {
onSuccess: (value) => `Success: ${value}`,
onFailure: (cause) => `Failure: ${Cause.pretty(cause)}`
})
// ┌─── string
// ▼
const renderedErr = Exit.match(errExit, {
onSuccess: (value) => `Success: ${value}`,
onFailure: (cause) => `Failure: ${Cause.pretty(cause)}`
})
// 4) 直接回傳 Exit(多用於測試或模擬)
// ┌─── Exit<number, never>
// ▼
const directSuccess = Exit.succeed(1)
// ┌─── Exit<never, string>
// ▼
const directFailure = Exit.failCause(Cause.fail("boom"))
// 輸出:
// renderedOk: Success: 42
// renderedErr: Failure: Error: my error
// directSuccess: { _id: 'Exit', _tag: 'Success', value: 1 }
// directFailure: {
// _id: 'Exit',
// _tag: 'Failure',
// cause: { _id: 'Cause', _tag: 'Fail', failure: 'boom' }
// }- 說明
Effect.runSyncExit(effect): 同步執行並回傳Exit;適合純同步流程或在測試/邊界處理結果。Exit.match(exit, { onSuccess, onFailure }): 將兩種狀態一次處理;常配合Cause.pretty取得可讀字串。Exit.succeed / Exit.failCause: 不經執行,直接建立結果值(常用於測試)。
Cause:失敗的結構化語言(Fail/Die/Interrupt/Sequential/Parallel)
- 用途
Cause以可組合的方式完整描述失敗:可區分預期錯誤(Fail)、缺陷(Die)、中斷(Interrupt),並保留錯誤在順序(Sequential)與並行(Parallel)發生時的關聯與脈絡。
- 適用情境
- 觀測與記錄:需要完整失敗脈絡(堆疊、並行多錯誤)以便 debug。
- 低階基礎設施:重試、監控、錯誤整形、報表。
常見操作
import { Effect, Cause } from "effect";
// 1) 建立帶有不同 Cause 的 Effect
// - Fail(可預期錯誤,會決定錯誤通道型別 E)
// - Die(缺陷,不會影響 E,因此 error channel 為 never)
const asDie = Effect.failCause(Cause.die(new Error("Boom!"))) // Effect<never, never, never>
const asFail = Effect.failCause(Cause.fail("Oops")) // Effect<never, string, never>// 2) 取得 Effect 的 Cause 並做處理(集中觀測)
const program = Effect.fail("error 1")
const allCauseEffect = Effect.catchAllCause(program, (cause) =>
Effect.succeed({
pretty: Cause.pretty(cause), // string
failures: Cause.failures(cause), // Chunk<string>
defects: Cause.defects(cause) // Chunk<unknown>
}))
const allCause = Effect.runSync(allCauseEffect)
console.log("all causes:", allCause)
// 輸出:
// all causes: {
// pretty: 'Error: error 1',
// failures: { _id: 'Chunk', values: [ 'error 1' ] },
// defects: { _id: 'Chunk', values: [] }
// }延續上面程式碼,我們可以對 Cause 進行模式比對,來處理不同的失敗情境。
// 3) 實務:集中觀測 + 模式比對(邊界收斂)
// 範例程式:你可以替換成實際工作流程的 Effect
const program = Effect.fail("error 1")
const handled = Effect.catchAllCause(program, (cause) => {
// 先「集中觀測」Cause 的全貌(供記錄或追蹤)
const observed = { // 將 Cause 轉為易讀/可處理的摘要
pretty: Cause.pretty(cause), // 轉為可讀多行字串,適合 console/log
failures: Array.from(Cause.failures(cause)), // 可預期錯誤(由 fail 等)
defects: Array.from(Cause.defects(cause)) // 非預期錯誤/缺陷(die 等)
}
// 再用「模式比對」把失敗分支化,收斂成邊界需要的結構
const result = Cause.match(cause, {
onFail: (e) => ({ status: 400, message: String(e) }),
onDie: () => ({ status: 500, message: "Unexpected defect" }),
onInterrupt: () => ({ status: 504, message: "Timeout" }),
onEmpty: { status: 200, message: "OK" },
onSequential: (l, r) => ({ status: 500, message: `seq: ${l.message} -> ${r.message}` }),
onParallel: (l, r) => ({ status: 500, message: `par: ${l.message} | ${r.message}` })
})
return Effect.succeed({ observed, result })
})
console.log("集中觀測 + 模式比對:", Effect.runSync(handled))
// 輸出:
// 集中觀測 + 模式比對: {
// observed: {
// pretty: 'Error: error 1',
// failures: [ 'error 1' ],
// defects: []
// },
// result: { status: 400, message: 'error 1' }
// }// 4) Empty:中性值與模式比對
import { Cause } from "effect";
// 建立 Empty(中性值),常用於合併的單位元或在測試中明確表示「沒有失敗」
const empty = Cause.empty
// 對 Empty 進行模式比對:會命中 onEmpty 分支
const label = Cause.match(empty, {
onEmpty: "Empty",
onFail: () => "Fail",
onDie: () => "Die",
onInterrupt: () => "Interrupt",
onSequential: () => "Sequential",
onParallel: () => "Parallel"
})// 5) Interrupt:超時情境(避免意外重試)
import { Effect, Cause, Duration } from "effect";
// 模擬外部呼叫:需要 2 秒才會失敗
const callPayment = Effect.delay(Effect.fail("gateway overloaded"), Duration.seconds(2))
// 加上 1 秒超時(逾時會產生 Interrupt)
const withTimeout = Effect.timeout(callPayment, Duration.seconds(1))
// 只要是 Interrupt,我們回傳 504/不要重試;Fail 則可依業務決策重試
const InterruptHandled = Effect.catchAllCause(withTimeout, (cause) =>
Effect.succeed(
Cause.match(cause, {
onInterrupt: () => ({ status: 504, message: "Payment Timeout" }),
onFail: (e) => ({ status: 400, message: String(e) }),
onDie: () => ({ status: 500, message: "Unexpected defect" }),
onEmpty: { status: 200, message: "OK" },
onSequential: (l, r) => ({ status: 500, message: `seq: ${l.message} -> ${r.message}` }),
onParallel: (l, r) => ({ status: 500, message: `par: ${l.message} | ${r.message}` })
})
))
Effect.runPromise(InterruptHandled).then((result) => {
console.log("Interrupt Handled:", result)
})
// 輸出:
// Interrupt Handled: {
// status: 400,
// message: "TimeoutException: Operation timed out after '1s'"
// }// 6) Parallel:並行批次(一次看見多個錯誤)
// 同時呼叫多個後端(可能同時失敗)
const userCall = Effect.fail("user: db unavailable")
const ordersCall = Effect.die(new Error("orders: decoder bug"))
const parallelCalls = Effect.all([userCall, ordersCall], { concurrency: 2 })
const report = Effect.catchAllCause(parallelCalls, (cause) =>
Effect.succeed({
failures: Array.from(Cause.failures(cause)),
defects: Array.from(Cause.defects(cause))
}))
Effect.runPromiseExit(report).then(console.log)
// 輸出:
// {
// _id: 'Exit',
// _tag: 'Success',
// value: {
// failures: [ 'user: db unavailable' ],
// defects: [
// Error: orders: decoder bug
// at <anonymous> (/Users/eric/personal-project/effect-app/src/day25/Program.ts:181:33)
// at ModuleJob.run (node:internal/modules/esm/module_job:345:25)
// at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:651:26)
// at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)
// ]
// }
// }// 7) Sequential:順序補救也失敗
// 先失敗 A,catch 後嘗試補救 B,但 B 也失敗 → Sequential(A -> B)
const programSeq = Effect.failCause(
Cause.fail("Oh no!") // A
).pipe(
Effect.ensuring(Effect.failCause(Cause.die("Boom!"))) // B
)
Effect.runPromiseExit(programSeq).then(console.log)
// 輸出:
// {
// _id: 'Exit',
// _tag: 'Failure', cause: {
// _id: 'Cause',
// _tag: 'Sequential',
// left: { _id: 'Cause', _tag: 'Fail', failure: 'Oh no!' },
// right: { _id: 'Cause', _tag: 'Die', defect: 'Boom!' }
// }
// }Cause:設計原則(Principles)
- 明確區分失敗類型:以 Fail/Die/Interrupt 切分語意,避免「一種錯誤走天下」導致策略混淆(例如把逾時誤判為可重試)。
- 保留組合脈絡:Sequential/Parallel 保留錯誤在流程中的結構,不壓平成單一訊息,讓除錯與追蹤具備可讀的「錯誤樹」。
- 好觀測、好統計:錯誤被做成可組合的資料格式,可用
pretty/failures/defects/match自動分類與彙整,方便做監控看板與報表。 - 一致的抽象:從單一錯誤到並行多錯誤,皆以同一
Cause抽象表示,跨域(批次、工作系統、微服務匯流)時仍可組合與比對。 - 策略更安全:錯誤細節不會丟、資料結構是可比對的,因此可以在某處清楚訂出「要不要重試、何時告警、如何補償、對應哪個回應碼」,提升程式碼可讀性與可維護性。
Cause:語意速查表(Semantics Cheatsheet)
| 類型 | 定義 | 常見來源 | 建議策略 |
|---|---|---|---|
| Empty | 沒有任何失敗(空值) | 合併多個結果時用的「空錯誤」 | 通常可忽略,無需處理 |
| Interrupt | Fiber 被中斷(cancel/timeout/scope 結束) | 逾時、使用者取消、作用域結束 | 不重試;回應「已取消/逾時」;映射對應狀態碼/UI 文案 |
| Sequential | 先失敗 A,補救 B 又失敗(Sequential(A -> B)) | catch/ensuring 中再次失敗 | 保留脈絡;聚焦後一失敗的告警與補救;檢討補救流程 |
| Parallel | 並行分支同時失敗(Parallel(L | R)) | 並行工作、批次任務 | 彙整顯示/上報;逐一判定是否重試與補償 |
策略指引(Decision Guide)
- 重新嘗試:只考慮「可恢復」的失敗來源;對 Interrupt 不重試,對缺陷(Die)改以修正程式為主。
- 回應語意:Interrupt 對應「已取消/逾時」,Sequential/Parallel 保留結構以供觀測與報表(錯誤樹)。
- 監控與告警:Sequential 著重「第二段失敗」;Parallel 需同時關注多個根因並做彙整告警。
- 可讀性與除錯:保持 Cause 結構,不壓平成字串,讓跨服務的失敗能被比對與追蹤。
總結
本文說明 Data Types 與 Effect 的分工:前者以純資料建立可能狀態,後者描述執行與副作用。也依序介紹了 Option、Either、Exit、Cause 的用法與實際應用場景。但這些只是 Effect 中的一部分 Data Types,下一篇會繼續介紹剩下幾種常見的 Data Types。天啊~還真有點多😅。但沒關係,我們一起撐下去吧!
參考資料
Last updated on