為何需要 Data Types?
Effect<R, E, A>
的 E 或 A)。接下來我們來介紹一些常用的 Data Types 吧~
data types 應用情境太多了。不過其實都很簡單,所以我們只會透過一些常見的例子來簡單介紹用法。如果你想要了解更多,可以參考官方文件。
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
以利後續組合。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()
,但更精煉且可重用。map/flatMap/match
串接。email
),但其值型別為 Option<string>
,表示可能沒有值。interface User = {
readonly id: number;
readonly username: string;
readonly email: Option.Option<string>;
};
email?: string
的好處Option<T>
下,key 固定存在(結構穩定);email?
可能連鍵都缺席。Option
以 Some/None
明確表示有值/無值,可被模式比對;email?
僅以鍵缺席或 undefined
表達,語意容易分散。Option
具備 map/flatMap/match/getOrElse
等 API,能以表達式風格串接;email?
常需要零散的 if/?./??
邏輯。Option
迫使呼叫端顯式處理 None
,降低漏判空值的機率;email?
容易在流程中被忽略。Option
讓結構穩定、語意清楚;對外輸出(API/JSON)再轉成可選鍵或 null
。用途
適用情境
說明
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.Success<A>
或 Exit.Failure<Cause<E>>
。概念上近似 Either<A, Cause<E>>
。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)發生時的關聯與脈絡。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!' }
// }
// }
pretty
/ failures
/ defects
/ match
自動分類與彙整,方便做監控看板與報表。Cause
抽象表示,跨域(批次、工作系統、微服務匯流)時仍可組合與比對。類型 | 定義 | 常見來源 | 建議策略 |
---|---|---|---|
Empty | 沒有任何失敗(空值) | 合併多個結果時用的「空錯誤」 | 通常可忽略,無需處理 |
Interrupt | Fiber 被中斷(cancel/timeout/scope 結束) | 逾時、使用者取消、作用域結束 | 不重試;回應「已取消/逾時」;映射對應狀態碼/UI 文案 |
Sequential | 先失敗 A,補救 B 又失敗(Sequential(A -> B) ) |
catch/ensuring 中再次失敗 | 保留脈絡;聚焦後一失敗的告警與補救;檢討補救流程 |
Parallel | 並行分支同時失敗(Parallel(L | R) ) |
並行工作、批次任務 | 彙整顯示/上報;逐一判定是否重試與補償 |
本文說明 Data Types 與 Effect 的分工:前者以純資料建立可能狀態,後者描述執行與副作用。也依序介紹了 Option、Either、Exit、Cause 的用法與實際應用場景。但這些只是 Effect 中的一部分 Data Types,下一篇會繼續介紹剩下幾種常見的 Data Types。天啊~還真有點多😅。但沒關係,我們一起撐下去吧!