筆記 - Effect Essentials
effect.website
Effect – The best way to build robust apps in TypeScript
Effect is a powerful TypeScript library designed to help developers easily create complex, synchronous, and asynchronous programs.

The Library
這個套件最主要的目的是讓開發者有著 Type-Safety 的同時,簡化處理 error handling。
Effect 還提供了 API 取代平時會裝的 Utility / Validation / Logger,並支援不同的環境如 Browser / Node / Edge。
所以可以把 Effect 想像成 TypeScript 的 Framework。
Concept
‘an Effect is a description of a program that is lazy and immutable.‘
Program
這裡指的 Program 不代表整個應用,而是應用的一部份。
Lazy
代表它不會自動執行,只有明確的指令告訴它執行,他才會運行。
這個特性實現了:
- 組合操作:可以將多個 Effect 組合再一起,需要的時候再執行他們。
- 控制時機:只有當真正需要時,你才會執行它。
Immutable
Effect 是不可變的,就如一段 Code 一樣,一但執行了,它就不會再改變了。
不可變的好處:
- 安全性:防止程式的其他部分意外的改變它的狀態。
The Effect Type
Effect<Success, Error, Requirements>
是 Effect 的核心型別,它有 3 個部分:
- Success - 成功時返回值的型別。為
void
時代表沒有返回值,為never
時代表會一直執行或者直到失敗。 - Error - 失敗時的錯誤型別。為
never
時代表沒有不會有任何錯誤發生。 - Requirements - 執行時時所需要的 Context 或 依賴資料 的型別。為
never
時代表沒有任何依賴。
Requirements 的資料會存在在 Effect
的 Context
Collection 中。
Effect ecosystem 通常使用 A 代表 Success,E 代表 Error,R 代表 Requirements。
Effect
是不可變的,所有操作都會返回新的Effect
,不會修改原有值。 ( Immutable )Effect
值描述了與外部世界交互的 Effect,但不會執行。 ( Lazy )Effect
需要通過 Effect Runtime System 來解釋和執行。Effect
的執行應該集中在應用的 Entry Point,以保持程式執行的可控性和清晰度。
Creating Effects
Why Not Throw Errors ?
const
const divide: (a: number, b: number) => number
divide = (a: number
a: number,b: number
b: number): number => { if (b: number
b === 0) { throw newError('Cannot divide by zero') } return
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
a: number
a /b: number
b }
無法在 function 明確表示可能會出現的 Errors。
在 Effect 可以使用 Effect.fail
和 Effect.succeed
讓 function 有更明確的描述。
Succeed
const
const success: Effect.Effect<number, never, never>
success =import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(8964)
- 總是成功:成功時返回一個
number
型別的值 - 不會失敗
- 無需依賴
Fail
const
const failure: Effect.Effect<never, Error, never>
failure =import Effect
Effect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>
fail(newError('Something went wrong'))
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
- 不會成功
- 可能失敗:執行時拋出的錯誤為
Error
型別 - 無需依賴
Example with Succeed and Fail
const
const divide: (a: number, b: number) => Effect.Effect<number, Error>
divide = (a: number
a: number,b: number
b: number):import Effect
Effect.interface Effect<out A, out E = never, out R = never>
Effect<number, Error> =>b: number
b === 0 ?import Effect
Effect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>
fail(newError('Cannot divide by zero')) :
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(a: number
a /b: number
b) constconst resultEffect: Effect.Effect<number, Error, never>
resultEffect =const divide: (a: number, b: number) => Effect.Effect<number, Error>
divide(10, 2)
resultEffect
的型別為 Effect<number, Error>
,代表它可能會產生成功的 number
或失敗的 Error
。
Modeling Synchronous Effects
在 JS 可以使用 thunks 實現延遲的計算。
thunks
是一個不接受任何參數的函數並可能返回一些值。
為了模擬同步效果,Effect 提供了 sync
與 try
並接受一個 thunk。
sync
const
const log: (message: string) => Effect.Effect<string, never, never>
log = (message: string
message: string) =>import Effect
Effect.const sync: <string>(evaluate: LazyArg<string>) => Effect.Effect<string, never, never>
sync(() => {var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log(message: string
message) // side effect return 'Hello, World!' }) constconst program: Effect.Effect<string, never, never>
program =const log: (message: string) => Effect.Effect<string, never, never>
log('Hello, World!')
- 成功時返回
string
型別 - 不會失敗
- 無需依賴
sync
不應該拋出任何錯誤,若需要則應考慮使用 try
。
try
const
const parse: (input: string) => Effect.Effect<any, Error, never>
parse = (input: string
input: string) =>import Effect
Effect.try({
try<any, Error>(options: { readonly try: LazyArg<any>; readonly catch: (error: unknown) => Error; }): Effect.Effect<any, Error, never> (+1 overload) export try
try: LazyArg<any>
try: () =>var JSON: JSON
JSON.JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): any
parse(input: string
input),catch: (error: unknown) => Error
catch: (unknown: unknown
unknown) => newError(`something went wrong ${
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
unknown: unknown
unknown}`) }) constconst program: Effect.Effect<any, Error, never>
program =const parse: (input: string) => Effect.Effect<any, Error, never>
parse('')
- 成功時返回 JSON.parse 的型別
any
- 失敗時返回
Error
型別 - 無需依賴
Modeling Asynchronous Effects
promise
與 tryPromise
一樣接收一個 thunk 但返回值是 Promise
。
promise
const
const delay: (message: string) => Effect.Effect<string, never, never>
delay = (message: string
message: string) =>import Effect
Effect.const promise: <string>(evaluate: (signal: AbortSignal) => PromiseLike<string>) => Effect.Effect<string, never, never>
promise<string>( () => newPromise((
var Promise: PromiseConstructor new <string>(executor: (resolve: (value: string | PromiseLike<string>) => void, reject: (reason?: any) => void) => void) => Promise<string>
resolve: (value: string | PromiseLike<string>) => void
resolve) => {function setTimeout<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+2 overloads)
setTimeout(() => {resolve: (value: string | PromiseLike<string>) => void
resolve(message: string
message) }, 2000) }) ) constconst program: Effect.Effect<string, never, never>
program =const delay: (message: string) => Effect.Effect<string, never, never>
delay('Async operation completed successfully!')
- 成功時返回
string
型別 - 不會失敗
- 無需依賴
promise
不應該拋出任何錯誤,若需要則應考慮使用 tryPromise
。
tryPromise
const
const getTodo: (id: number) => Effect.Effect<Response, Error, never>
getTodo = (id: number
id: number) =>import Effect
Effect.tryPromise({
const tryPromise: <Response, Error>(options: { readonly try: (signal: AbortSignal) => PromiseLike<Response>; readonly catch: (error: unknown) => Error; }) => Effect.Effect<...> (+1 overload)
try: (signal: AbortSignal) => PromiseLike<Response>
try: () =>function fetch(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response> (+1 overload)
fetch(`https://jsonplaceholder.typicode.com/todos/${id: number
id}`),catch: (error: unknown) => Error
catch: (unknown: unknown
unknown) => newError(`something went wrong ${
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
unknown: unknown
unknown}`) }) constconst program: Effect.Effect<Response, Error, never>
program =const getTodo: (id: number) => Effect.Effect<Response, Error, never>
getTodo(1)
- 成功時返回
Response
型別 - 失敗時返回
Error
型別 - 無需依賴
From a callback
async
在使用舊版的 API 時,可能會遇到 API 只接受 callback
的情況,這時可以使用 async
。
const
const readFile: (filename: string) => Effect.Effect<Buffer, Error, never>
readFile = (filename: string
filename: string) =>import Effect
Effect.const async: <Buffer, Error, never>(register: (callback: (_: Effect.Effect<Buffer, Error, never>) => void, signal: AbortSignal) => void | Effect.Effect<void, never, never>, blockingOn?: FiberId) => Effect.Effect<...>
async<Buffer, Error>((resume: (_: Effect.Effect<Buffer, Error, never>) => void
resume) => {module "node:fs"
NodeFS.function readFile(path: NodeFS.PathOrFileDescriptor, callback: (err: NodeJS.ErrnoException | null, data: Buffer) => void): void (+3 overloads)
readFile(filename: string
filename, (error: NodeJS.ErrnoException | null
error,data: Buffer
data) => { if (error: NodeJS.ErrnoException | null
error) {resume: (_: Effect.Effect<Buffer, Error, never>) => void
resume(import Effect
Effect.const fail: <NodeJS.ErrnoException>(error: NodeJS.ErrnoException) => Effect.Effect<never, NodeJS.ErrnoException, never>
fail(error: NodeJS.ErrnoException
error)) } else {resume: (_: Effect.Effect<Buffer, Error, never>) => void
resume(import Effect
Effect.const succeed: <Buffer>(value: Buffer) => Effect.Effect<Buffer, never, never>
succeed(data: Buffer
data)) } }) }) constconst program: Effect.Effect<Buffer, Error, never>
program =const readFile: (filename: string) => Effect.Effect<Buffer, Error, never>
readFile("todos.txt")
- 成功時返回
Buffer
型別 - 失敗時返回
Error
型別 - 無需依賴
Suspended Effects
suspend
suspend
也是接收一個 thunk,但 thunk 要返回一個 Effect
。
// === Lazy Evaluation === let
let i: number
i = 0 constconst badEffect: Effect.Effect<number, never, never>
badEffect =import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(let i: number
i++) constconst goodEffect: Effect.Effect<number, never, never>
goodEffect =import Effect
Effect.const suspend: <number, never, never>(effect: LazyArg<Effect.Effect<number, never, never>>) => Effect.Effect<number, never, never>
suspend(() =>import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(let i: number
i++))import Effect
Effect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => number
runSync(const badEffect: Effect.Effect<number, never, never>
badEffect) // Output: 0import Effect
Effect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => number
runSync(const badEffect: Effect.Effect<number, never, never>
badEffect) // Output: 0import Effect
Effect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => number
runSync(const goodEffect: Effect.Effect<number, never, never>
goodEffect) // Output: 1import Effect
Effect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => number
runSync(const goodEffect: Effect.Effect<number, never, never>
goodEffect) // Output: 2
// === Handling Circular Dependencies === const
const blowsUp: (n: number) => Effect.Effect<number>
blowsUp = (n: number
n: number):import Effect
Effect.interface Effect<out A, out E = never, out R = never>
Effect<number> =>n: number
n < 2 ?import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(1) :import Effect
Effect.zipWith(
const zipWith: <number, never, never, number, never, never, number>(self: Effect.Effect<number, never, never>, that: Effect.Effect<number, never, never>, f: (a: number, b: number) => number, options?: { readonly concurrent?: boolean | undefined; readonly batching?: boolean | "inherit" | undefined; readonly concurrentFinalizers?: boolean | undefined; }) => Effect.Effect<...> (+1 overload)
const blowsUp: (n: number) => Effect.Effect<number>
blowsUp(n: number
n - 1),const blowsUp: (n: number) => Effect.Effect<number>
blowsUp(n: number
n - 2), (a: number
a,b: number
b) =>a: number
a +b: number
b)import Effect
Effect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => number
runSync(const blowsUp: (n: number) => Effect.Effect<number>
blowsUp(32)) // crash: JavaScript heap out of memory constconst allGood: (n: number) => Effect.Effect<number>
allGood = (n: number
n: number):import Effect
Effect.interface Effect<out A, out E = never, out R = never>
Effect<number> =>n: number
n < 2 ?import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(1) :import Effect
Effect.zipWith(
const zipWith: <number, never, never, number, never, never, number>(self: Effect.Effect<number, never, never>, that: Effect.Effect<number, never, never>, f: (a: number, b: number) => number, options?: { readonly concurrent?: boolean | undefined; readonly batching?: boolean | "inherit" | undefined; readonly concurrentFinalizers?: boolean | undefined; }) => Effect.Effect<...> (+1 overload)
import Effect
Effect.const suspend: <number, never, never>(effect: LazyArg<Effect.Effect<number, never, never>>) => Effect.Effect<number, never, never>
suspend(() =>const allGood: (n: number) => Effect.Effect<number>
allGood(n: number
n - 1)),import Effect
Effect.const suspend: <number, never, never>(effect: LazyArg<Effect.Effect<number, never, never>>) => Effect.Effect<number, never, never>
suspend(() =>const allGood: (n: number) => Effect.Effect<number>
allGood(n: number
n - 2)), (a: number
a,b: number
b) =>a: number
a +b: number
b )import Effect
Effect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => number
runSync(const allGood: (n: number) => Effect.Effect<number>
allGood(32)) // Output: 3524578
// === Unifying Return Type === const
const ugly: (a: number, b: number) => Effect.Effect<never, Error, never> | Effect.Effect<number, never, never>
ugly = (a: number
a: number,b: number
b: number) =>b: number
b === 0 ?import Effect
Effect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>
fail(newError("Cannot divide by zero")) :
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(a: number
a /b: number
b) constconst nice: (a: number, b: number) => Effect.Effect<number, Error, never>
nice = (a: number
a: number,b: number
b: number) =>import Effect
Effect.const suspend: <number, Error, never>(effect: LazyArg<Effect.Effect<number, Error, never>>) => Effect.Effect<number, Error, never>
suspend(() =>b: number
b === 0 ?import Effect
Effect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>
fail(newError("Cannot divide by zero")) :
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(a: number
a /b: number
b) )
Cheatsheet
Function | Given | To |
---|---|---|
succeed | A | Effect<A> |
fail | E | Effect<never, E> |
sync | () => A | Effect<A> |
try | () => A | Effect<A, UnknownException> |
try (overload) | () => A , unknown => E | Effect<A, E> |
promise | () => Promise<A> | Effect<A> |
tryPromise | () => Promise<A> | Effect<A, UnknownException> |
tryPromise (overload) | () => Promise<A> , unknown => E | Effect<A, E> |
async | (Effect<A, E> => void) => void | Effect<A, E> |
suspend | () => Effect<A, E, R> | Effect<A, E, R> |
Running Effects
要執行 Effect 需要使用 “run” 相關的 functions。
runSync
執行一個同步的 Effect,並返回結果。
const
const program: Effect.Effect<number, never, never>
program =import Effect
Effect.const sync: <number>(evaluate: LazyArg<number>) => Effect.Effect<number, never, never>
sync(() => {var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log("Hello, World!") return 1 }) constconst result: number
result =import Effect
Effect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => number
runSync(const program: Effect.Effect<number, never, never>
program) // Output: Hello, World!var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log(const result: number
result) // Output: 1
runSyncExit
與 runSync
一樣但返回的值為 Exit
。
const
const result1: Exit<number, never>
result1 =import Effect
Effect.const runSyncExit: <number, never>(effect: Effect.Effect<number, never, never>) => Exit<number, never>
runSyncExit(import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(1))var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log(const result1: Exit<number, never>
result1) /* Output: { _id: "Exit", _tag: "Success", value: 1 } */ constconst result2: Exit<never, string>
result2 =import Effect
Effect.const runSyncExit: <never, string>(effect: Effect.Effect<never, string, never>) => Exit<never, string>
runSyncExit(import Effect
Effect.const fail: <string>(error: string) => Effect.Effect<never, string, never>
fail("my error"))var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log(const result2: Exit<never, string>
result2) /* Output: { _id: "Exit", _tag: "Failure", cause: { _id: "Cause", _tag: "Fail", failure: "my error" } } */
runPromise
執行一個非同步的 Effect,並返回結果。
import Effect
Effect.runPromise(
const runPromise: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(1)).Promise<number>.then<void, never>(onfulfilled?: ((value: number) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) // Output: 1import Effect
Effect.runPromise(
const runPromise: <never, string>(effect: Effect.Effect<never, string, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<never>
import Effect
Effect.const fail: <string>(error: string) => Effect.Effect<never, string, never>
fail("my error")) // rejects
runPromiseExit
與 runPromise
一樣但返回的值為 Exit
。
import Effect
Effect.runPromiseExit(
const runPromiseExit: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<Exit<number, never>>
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(1)).Promise<Exit<number, never>>.then<void, never>(onfulfilled?: ((value: Exit<number, never>) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) /* Output: { _id: "Exit", _tag: "Success", value: 1 } */import Effect
Effect.runPromiseExit(
const runPromiseExit: <never, string>(effect: Effect.Effect<never, string, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<Exit<never, string>>
import Effect
Effect.const fail: <string>(error: string) => Effect.Effect<never, string, never>
fail("my error")).Promise<Exit<never, string>>.then<void, never>(onfulfilled?: ((value: Exit<never, string>) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) /* Output: { _id: "Exit", _tag: "Failure", cause: { _id: "Cause", _tag: "Fail", failure: "my error" } } */
runFork
使用 runFork
會返回 Fiber。
import {
import Effect
Effect,import Console
Console,import Schedule
Schedule,import Fiber
Fiber } from "effect" constconst program: Effect.Effect<number, never, never>
program =import Effect
Effect.const repeat: <void, never, never, number, never>(self: Effect.Effect<void, never, never>, schedule: Schedule.Schedule<number, void, never>) => Effect.Effect<number, never, never> (+3 overloads)
repeat(import Console
Console.const log: (...args: ReadonlyArray<any>) => Effect.Effect<void>
log("running..."),import Schedule
Schedule.const spaced: (duration: DurationInput) => Schedule.Schedule<number>
spaced("200 millis") ) constconst fiber: Fiber.RuntimeFiber<number, never>
fiber =import Effect
Effect.const runFork: <number, never>(effect: Effect.Effect<number, never, never>, options?: RunForkOptions) => Fiber.RuntimeFiber<number, never>
runFork(const program: Effect.Effect<number, never, never>
program)function setTimeout<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+2 overloads)
setTimeout(() => {import Effect
Effect.const runFork: <Exit<number, never>, never>(effect: Effect.Effect<Exit<number, never>, never, never>, options?: RunForkOptions) => Fiber.RuntimeFiber<...>
runFork(import Fiber
Fiber.const interrupt: <number, never>(self: Fiber.Fiber<number, never>) => Effect.Effect<Exit<number, never>, never, never>
interrupt(const fiber: Fiber.RuntimeFiber<number, never>
fiber)) }, 500)
Using Generators
使用 yield 取得 Effect 的值,可以把 yield Effect 當作 await Promise。
gen
return 的 Effect 包含裡面所有 yield Effect 的 error types。
// Function to add a small service charge to a transaction amount const
const addServiceCharge: (amount: number) => number
addServiceCharge = (amount: number
amount: number) =>amount: number
amount + 1 // Function to apply a discount safely to a transaction amount constconst applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>
applyDiscount = (total: number
total: number,discountRate: number
discountRate: number ):import Effect
Effect.interface Effect<out A, out E = never, out R = never>
Effect<number, Error> =>discountRate: number
discountRate === 0 ?import Effect
Effect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>
fail(newError("Discount rate cannot be zero")) :
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(total: number
total - (total: number
total *discountRate: number
discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount =import Effect
Effect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>
promise(() =>var Promise: PromiseConstructor
Promise.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)
resolve(100)) // Simulated asynchronous task to fetch a discount rate from a configuration file constconst fetchDiscountRate: Effect.Effect<number, never, never>
fetchDiscountRate =import Effect
Effect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>
promise(() =>var Promise: PromiseConstructor
Promise.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)
resolve(5)) // Assembling the program using a generator function constconst program: Effect.Effect<string, Error, never>
program =import Effect
Effect.const gen: <YieldWrap<Effect.Effect<number, Error, never>>, string>(f: (resume: Effect.Adapter) => Generator<YieldWrap<Effect.Effect<number, Error, never>>, string, never>) => Effect.Effect<...> (+1 overload)
gen(function* () { // Retrieve the transaction amount constconst transactionAmount: number
transactionAmount = yield*const fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount // Retrieve the discount rate constconst discountRate: number
discountRate = yield*const fetchDiscountRate: Effect.Effect<number, never, never>
fetchDiscountRate // Calculate discounted amount constconst discountedAmount: number
discountedAmount = yield*const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>
applyDiscount(const transactionAmount: number
transactionAmount,const discountRate: number
discountRate ) // Apply service charge constconst finalAmount: number
finalAmount =const addServiceCharge: (amount: number) => number
addServiceCharge(const discountedAmount: number
discountedAmount) // Return the total amount after applying the charge return `Final amount to charge: ${const finalAmount: number
finalAmount}` }) // Execute the program and log the resultimport Effect
Effect.runPromise(
const runPromise: <string, Error>(effect: Effect.Effect<string, Error, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<string>
const program: Effect.Effect<string, Error, never>
program).Promise<string>.then<void, never>(onfulfilled?: ((value: string) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) // Output: "Final amount to charge: 96"
Building Pipelines
使用 Pipelines 與 Generators 一樣可以執行 Effect。
pipe
pipe
可以傳入多個參數,第一個值為 input,其餘為 function。
import {
function pipe<A>(a: A): A (+19 overloads)
pipe } from "effect" constconst increment: (x: number) => number
increment = (x: number
x: number) =>x: number
x + 1 constconst double: (x: number) => number
double = (x: number
x: number) =>x: number
x * 2 constconst subtractTen: (x: number) => number
subtractTen = (x: number
x: number) =>x: number
x - 10 constconst result: number
result =pipe<5, number, number, number>(a: 5, ab: (a: 5) => number, bc: (b: number) => number, cd: (c: number) => number): number (+19 overloads)
pipe(5,const increment: (x: number) => number
increment,const double: (x: number) => number
double,const subtractTen: (x: number) => number
subtractTen)var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log(const result: number
result) // Output: 2
這個寫法等同於 subtractTen(double(increment(5)))
,但可讀性更好。
map
若 input 為 Effect,則可以使用 map
做轉換。
import {
function pipe<A>(a: A): A (+19 overloads)
pipe,import Effect
Effect } from "effect" // Function to add a small service charge to a transaction amount constconst addServiceCharge: (amount: number) => number
addServiceCharge = (amount: number
amount: number) =>amount: number
amount + 1 // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount =import Effect
Effect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>
promise(() =>var Promise: PromiseConstructor
Promise.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)
resolve(100)) // Apply service charge to the transaction amount constconst finalAmount: Effect.Effect<number, never, never>
finalAmount =pipe<Effect.Effect<number, never, never>, Effect.Effect<number, never, never>>(a: Effect.Effect<number, never, never>, ab: (a: Effect.Effect<number, never, never>) => Effect.Effect<...>): Effect.Effect<...> (+19 overloads)
pipe(const fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount,import Effect
Effect.const map: <number, number>(f: (a: number) => number) => <E, R>(self: Effect.Effect<number, E, R>) => Effect.Effect<number, E, R> (+1 overload)
map(const addServiceCharge: (amount: number) => number
addServiceCharge))import Effect
Effect.runPromise(
const runPromise: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
const finalAmount: Effect.Effect<number, never, never>
finalAmount).Promise<number>.then<void, never>(onfulfilled?: ((value: number) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) // Output: 101
map 支援以下 overload
const mappedEffect = pipe(myEffect, Effect.map(transformation)) const mappedEffect = Effect.map(myEffect, transformation) const mappedEffect = myEffect.pipe(Effect.map(transformation))
flatMap
與 map
相同但 function 的返回值可以是 Effect。
import {
function pipe<A>(a: A): A (+19 overloads)
pipe,import Effect
Effect } from "effect" // Function to apply a discount safely to a transaction amount constconst applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>
applyDiscount = (total: number
total: number,discountRate: number
discountRate: number ):import Effect
Effect.interface Effect<out A, out E = never, out R = never>
Effect<number, Error> =>discountRate: number
discountRate === 0 ?import Effect
Effect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>
fail(newError("Discount rate cannot be zero")) :
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(total: number
total - (total: number
total *discountRate: number
discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount =import Effect
Effect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>
promise(() =>var Promise: PromiseConstructor
Promise.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)
resolve(100)) constconst finalAmount: Effect.Effect<number, Error, never>
finalAmount =pipe<Effect.Effect<number, never, never>, Effect.Effect<number, Error, never>>(a: Effect.Effect<number, never, never>, ab: (a: Effect.Effect<number, never, never>) => Effect.Effect<...>): Effect.Effect<...> (+19 overloads)
pipe(const fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount,import Effect
Effect.const flatMap: <number, number, Error, never>(f: (a: number) => Effect.Effect<number, Error, never>) => <E, R>(self: Effect.Effect<number, E, R>) => Effect.Effect<number, Error | E, R> (+1 overload)
flatMap((amount: number
amount) =>const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>
applyDiscount(amount: number
amount, 5)) )import Effect
Effect.runPromise(
const runPromise: <number, Error>(effect: Effect.Effect<number, Error, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
const finalAmount: Effect.Effect<number, Error, never>
finalAmount).Promise<number>.then<void, never>(onfulfilled?: ((value: number) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) // Output: 95
overload 也與 map 相同
const flatMappedEffect = pipe(myEffect, Effect.flatMap(transformation)) const flatMappedEffect = Effect.flatMap(myEffect, transformation) const flatMappedEffect = myEffect.pipe(Effect.flatMap(transformation))
as
可以使用 as
直接轉換原本 Effect 的值。
const
const program: Effect.Effect<string, never, never>
program =pipe<Effect.Effect<number, never, never>, Effect.Effect<string, never, never>>(a: Effect.Effect<number, never, never>, ab: (a: Effect.Effect<number, never, never>) => Effect.Effect<...>): Effect.Effect<...> (+19 overloads)
pipe(import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(5),import Effect
Effect.const as: <string>(value: string) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<string, E, R> (+1 overload)
as("new value"))import Effect
Effect.runPromise(
const runPromise: <string, never>(effect: Effect.Effect<string, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<string>
const program: Effect.Effect<string, never, never>
program).Promise<string>.then<void, never>(onfulfilled?: ((value: string) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) // Output: "new value"
andThen
算是以上 map
, flatMap
和 as
的組合,
const transformedEffect = pipe(myEffect, Effect.andThen(anotherEffect)) const transformedEffect = Effect.andThen(myEffect, anotherEffect) const transformedEffect = myEffect.pipe(Effect.andThen(anotherEffect))
andThen
有 6 種 overload:
- 一個值 (
as
) - function 返回一個值 (
map
) - 一個 Promise
- function 返回一個 Promise
- 一個 Effect
- function 返回一個 Effect
tap
tap
的使用方法類似 map
/ flatMap
但是忽略返回值。
// Function to apply a discount safely to a transaction amount const
const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>
applyDiscount = (total: number
total: number,discountRate: number
discountRate: number ):import Effect
Effect.interface Effect<out A, out E = never, out R = never>
Effect<number, Error> =>discountRate: number
discountRate === 0 ?import Effect
Effect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>
fail(newError("Discount rate cannot be zero")) :
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
import Effect
Effect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>
succeed(total: number
total - (total: number
total *discountRate: number
discountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount =import Effect
Effect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>
promise(() =>var Promise: PromiseConstructor
Promise.PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)
resolve(100)) constconst finalAmount: Effect.Effect<number, Error, never>
finalAmount =pipe<Effect.Effect<number, never, never>, Effect.Effect<number, never, never>, Effect.Effect<number, Error, never>>(a: Effect.Effect<number, never, never>, ab: (a: Effect.Effect<...>) => Effect.Effect<...>, bc: (b: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+19 overloads)
pipe(const fetchTransactionAmount: Effect.Effect<number, never, never>
fetchTransactionAmount,import Effect
Effect.const tap: <number, Effect.Effect<void, never, never>>(f: (a: number) => Effect.Effect<void, never, never>) => <E, R>(self: Effect.Effect<number, E, R>) => Effect.Effect<...> (+7 overloads)
tap((amount: number
amount) =>import Effect
Effect.const sync: <void>(evaluate: LazyArg<void>) => Effect.Effect<void, never, never>
sync(() =>var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log(`Apply a discount to: ${amount: number
amount}`)) ), // `amount` is still available!import Effect
Effect.const flatMap: <number, number, Error, never>(f: (a: number) => Effect.Effect<number, Error, never>) => <E, R>(self: Effect.Effect<number, E, R>) => Effect.Effect<number, Error | E, R> (+1 overload)
flatMap((amount: number
amount) =>const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>
applyDiscount(amount: number
amount, 5)) )import Effect
Effect.runPromise(
const runPromise: <number, Error>(effect: Effect.Effect<number, Error, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
const finalAmount: Effect.Effect<number, Error, never>
finalAmount).Promise<number>.then<void, never>(onfulfilled?: ((value: number) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
then(var console: Console
console.Console.log(...data: any[]): void (+1 overload)
log) /* Output: Apply a discount to: 100 95 */
all
all
可以用陣列傳入多個 Effect,並以陣列返回其結果。
需要注意的是,雖然它與 Promise.all
看起來相似,但 Effect.all
是按順序執行的。
import { Effect } from "effect" const combinedEffect = Effect.all([effect1, effect2, ...])
// Simulated function to read configuration from a file const
webConfig =
const webConfig: Effect.Effect<{ dbConnection: string; port: number; }, never, never>
import Effect
Effect.promise(() =>
const promise: <{ dbConnection: string; port: number; }>(evaluate: (signal: AbortSignal) => PromiseLike<{ dbConnection: string; port: number; }>) => Effect.Effect<{ dbConnection: string; port: number; }, never, never>
var Promise: PromiseConstructor
Promise.resolve({
PromiseConstructor.resolve<{ dbConnection: string; port: number; }>(value: { dbConnection: string; port: number; }): Promise<{ dbConnection: string; port: number; }> (+2 overloads)
dbConnection: string
dbConnection: "localhost",port: number
port: 8080 }) ) // Simulated function to test database connectivity constconst checkDatabaseConnectivity: Effect.Effect<string, never, never>
checkDatabaseConnectivity =import Effect
Effect.const promise: <string>(evaluate: (signal: AbortSignal) => PromiseLike<string>) => Effect.Effect<string, never, never>
promise(() =>var Promise: PromiseConstructor
Promise.PromiseConstructor.resolve<string>(value: string): Promise<string> (+2 overloads)
resolve("Connected to Database") ) // Combine both effects to perform startup checks conststartupChecks =
const startupChecks: Effect.Effect<[{ dbConnection: string; port: number; }, string], never, never>
import Effect
Effect.all([
const all: <readonly [Effect.Effect<{ dbConnection: string; port: number; }, never, never>, Effect.Effect<string, never, never>], { readonly concurrency?: Concurrency | undefined; readonly batching?: boolean | "inherit" | undefined; readonly discard?: boolean | undefined; readonly mode?: "default" | "validate" | "either" | undefined; readonly concurrentFinalizers?: boolean | undefined; }>(arg: readonly [...], options?: { ...; } | undefined) => Effect.Effect<...>
webConfig,
const webConfig: Effect.Effect<{ dbConnection: string; port: number; }, never, never>
const checkDatabaseConnectivity: Effect.Effect<string, never, never>
checkDatabaseConnectivity])import Effect
Effect.runPromise(
const runPromise: <[{ dbConnection: string; port: number; }, string], never>(effect: Effect.Effect<[{ dbConnection: string; port: number; }, string], never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<...>
startupChecks).
const startupChecks: Effect.Effect<[{ dbConnection: string; port: number; }, string], never, never>
then(([
Promise<[{ dbConnection: string; port: number; }, string]>.then<void, never>(onfulfilled?: ((value: [{ dbConnection: string; port: number; }, string]) => void | PromiseLike<void>) | null | undefined, onrejected?: ((reason: any) => PromiseLike<never>) | null | undefined): Promise<...>
config,
config: { dbConnection: string; port: number; }
dbStatus: string
dbStatus]) => {var console: Console
console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
log( `Configuration: ${var JSON: JSON
JSON.JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)
stringify(config)}, DB Status: ${
config: { dbConnection: string; port: number; }
dbStatus: string
dbStatus}` ) }) /* Output: Configuration: {"dbConnection":"localhost","port":8080}, DB Status: Connected to Database */
Cheatsheet
Function | Input | Output |
---|---|---|
map | Effect<A, E, R> , A => B | Effect<B, E, R> |
flatMap | Effect<A, E, R> , A => Effect<B, E, R> | Effect<B, E, R> |
andThen | Effect<A, E, R> , * | Effect<B, E, R> |
tap | Effect<A, E, R> , A => Effect<B, E, R> | Effect<A, E, R> |
all | [Effect<A, E, R>, Effect<B, E, R>, ...] | Effect<[A, B, ...], E, R> |