筆記 - 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 ?
constconst divide: (a: number, b: number) => numberdivide = (a: numbera: number,b: numberb: number): number => { if (b: numberb === 0) { throw newError('Cannot divide by zero') } returnvar Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)a: numbera /b: numberb }
無法在 function 明確表示可能會出現的 Errors。
在 Effect 可以使用 Effect.fail 和 Effect.succeed 讓 function 有更明確的描述。
Succeed
constconst success: Effect.Effect<number, never, never>success =import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(8964)
- 總是成功:成功時返回一個
number型別的值 - 不會失敗
- 無需依賴
Fail
constconst failure: Effect.Effect<never, Error, never>failure =import EffectEffect.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
constconst divide: (a: number, b: number) => Effect.Effect<number, Error>divide = (a: numbera: number,b: numberb: number):import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<number, Error> =>b: numberb === 0 ?import EffectEffect.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 EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(a: numbera /b: numberb) 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
constconst log: (message: string) => Effect.Effect<string, never, never>log = (message: stringmessage: string) =>import EffectEffect.const sync: <string>(evaluate: LazyArg<string>) => Effect.Effect<string, never, never>sync(() => {var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(message: stringmessage) // 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
constconst parse: (input: string) => Effect.Effect<any, Error, never>parse = (input: stringinput: string) =>import EffectEffect.try({try<any, Error>(options: { readonly try: LazyArg<any>; readonly catch: (error: unknown) => Error; }): Effect.Effect<any, Error, never> (+1 overload) export trytry: LazyArg<any>try: () =>var JSON: JSONJSON.JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): anyparse(input: stringinput),catch: (error: unknown) => Errorcatch: (unknown: unknownunknown) => newError(`something went wrong ${var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)unknown: unknownunknown}`) }) 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
constconst delay: (message: string) => Effect.Effect<string, never, never>delay = (message: stringmessage: string) =>import EffectEffect.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>) => voidresolve) => {function setTimeout<[]>(callback: () => void, ms?: number): NodeJS.Timeout (+2 overloads)setTimeout(() => {resolve: (value: string | PromiseLike<string>) => voidresolve(message: stringmessage) }, 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
constconst getTodo: (id: number) => Effect.Effect<Response, Error, never>getTodo = (id: numberid: number) =>import EffectEffect.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: numberid}`),catch: (error: unknown) => Errorcatch: (unknown: unknownunknown) => newError(`something went wrong ${var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)unknown: unknownunknown}`) }) 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。
constconst readFile: (filename: string) => Effect.Effect<Buffer, Error, never>readFile = (filename: stringfilename: string) =>import EffectEffect.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>) => voidresume) => {module "node:fs"NodeFS.function readFile(path: NodeFS.PathOrFileDescriptor, callback: (err: NodeJS.ErrnoException | null, data: Buffer) => void): void (+3 overloads)readFile(filename: stringfilename, (error: NodeJS.ErrnoException | nullerror,data: Bufferdata) => { if (error: NodeJS.ErrnoException | nullerror) {resume: (_: Effect.Effect<Buffer, Error, never>) => voidresume(import EffectEffect.const fail: <NodeJS.ErrnoException>(error: NodeJS.ErrnoException) => Effect.Effect<never, NodeJS.ErrnoException, never>fail(error: NodeJS.ErrnoExceptionerror)) } else {resume: (_: Effect.Effect<Buffer, Error, never>) => voidresume(import EffectEffect.const succeed: <Buffer>(value: Buffer) => Effect.Effect<Buffer, never, never>succeed(data: Bufferdata)) } }) }) 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 === letlet i: numberi = 0 constconst badEffect: Effect.Effect<number, never, never>badEffect =import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(let i: numberi++) constconst goodEffect: Effect.Effect<number, never, never>goodEffect =import EffectEffect.const suspend: <number, never, never>(effect: LazyArg<Effect.Effect<number, never, never>>) => Effect.Effect<number, never, never>suspend(() =>import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(let i: numberi++))import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const badEffect: Effect.Effect<number, never, never>badEffect) // Output: 0import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const badEffect: Effect.Effect<number, never, never>badEffect) // Output: 0import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const goodEffect: Effect.Effect<number, never, never>goodEffect) // Output: 1import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const goodEffect: Effect.Effect<number, never, never>goodEffect) // Output: 2
// === Handling Circular Dependencies === constconst blowsUp: (n: number) => Effect.Effect<number>blowsUp = (n: numbern: number):import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<number> =>n: numbern < 2 ?import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(1) :import EffectEffect.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: numbern - 1),const blowsUp: (n: number) => Effect.Effect<number>blowsUp(n: numbern - 2), (a: numbera,b: numberb) =>a: numbera +b: numberb)import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const blowsUp: (n: number) => Effect.Effect<number>blowsUp(32)) // crash: JavaScript heap out of memory constconst allGood: (n: number) => Effect.Effect<number>allGood = (n: numbern: number):import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<number> =>n: numbern < 2 ?import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(1) :import EffectEffect.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 EffectEffect.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: numbern - 1)),import EffectEffect.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: numbern - 2)), (a: numbera,b: numberb) =>a: numbera +b: numberb )import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const allGood: (n: number) => Effect.Effect<number>allGood(32)) // Output: 3524578
// === Unifying Return Type === constconst ugly: (a: number, b: number) => Effect.Effect<never, Error, never> | Effect.Effect<number, never, never>ugly = (a: numbera: number,b: numberb: number) =>b: numberb === 0 ?import EffectEffect.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 EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(a: numbera /b: numberb) constconst nice: (a: number, b: number) => Effect.Effect<number, Error, never>nice = (a: numbera: number,b: numberb: number) =>import EffectEffect.const suspend: <number, Error, never>(effect: LazyArg<Effect.Effect<number, Error, never>>) => Effect.Effect<number, Error, never>suspend(() =>b: numberb === 0 ?import EffectEffect.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 EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(a: numbera /b: numberb) )
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,並返回結果。
constconst program: Effect.Effect<number, never, never>program =import EffectEffect.const sync: <number>(evaluate: LazyArg<number>) => Effect.Effect<number, never, never>sync(() => {var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log("Hello, World!") return 1 }) constconst result: numberresult =import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const program: Effect.Effect<number, never, never>program) // Output: Hello, World!var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(const result: numberresult) // Output: 1
runSyncExit
與 runSync 一樣但返回的值為 Exit。
constconst result1: Exit<number, never>result1 =import EffectEffect.const runSyncExit: <number, never>(effect: Effect.Effect<number, never, never>) => Exit<number, never>runSyncExit(import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(1))var console: Consoleconsole.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 EffectEffect.const runSyncExit: <never, string>(effect: Effect.Effect<never, string, never>) => Exit<never, string>runSyncExit(import EffectEffect.const fail: <string>(error: string) => Effect.Effect<never, string, never>fail("my error"))var console: Consoleconsole.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 EffectEffect.runPromise(const runPromise: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>import EffectEffect.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: Consoleconsole.Console.log(...data: any[]): void (+1 overload)log) // Output: 1import EffectEffect.runPromise(const runPromise: <never, string>(effect: Effect.Effect<never, string, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<never>import EffectEffect.const fail: <string>(error: string) => Effect.Effect<never, string, never>fail("my error")) // rejects
runPromiseExit
與 runPromise 一樣但返回的值為 Exit。
import EffectEffect.runPromiseExit(const runPromiseExit: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<Exit<number, never>>import EffectEffect.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: Consoleconsole.Console.log(...data: any[]): void (+1 overload)log) /* Output: { _id: "Exit", _tag: "Success", value: 1 } */import EffectEffect.runPromiseExit(const runPromiseExit: <never, string>(effect: Effect.Effect<never, string, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<Exit<never, string>>import EffectEffect.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: Consoleconsole.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 EffectEffect,import ConsoleConsole,import ScheduleSchedule,import FiberFiber } from "effect" constconst program: Effect.Effect<number, never, never>program =import EffectEffect.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 ConsoleConsole.const log: (...args: ReadonlyArray<any>) => Effect.Effect<void>log("running..."),import ScheduleSchedule.const spaced: (duration: DurationInput) => Schedule.Schedule<number>spaced("200 millis") ) constconst fiber: Fiber.RuntimeFiber<number, never>fiber =import EffectEffect.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 EffectEffect.const runFork: <Exit<number, never>, never>(effect: Effect.Effect<Exit<number, never>, never, never>, options?: RunForkOptions) => Fiber.RuntimeFiber<...>runFork(import FiberFiber.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 constconst addServiceCharge: (amount: number) => numberaddServiceCharge = (amount: numberamount: number) =>amount: numberamount + 1 // Function to apply a discount safely to a transaction amount constconst applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>applyDiscount = (total: numbertotal: number,discountRate: numberdiscountRate: number ):import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<number, Error> =>discountRate: numberdiscountRate === 0 ?import EffectEffect.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 EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(total: numbertotal - (total: numbertotal *discountRate: numberdiscountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>fetchTransactionAmount =import EffectEffect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>promise(() =>var Promise: PromiseConstructorPromise.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 EffectEffect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>promise(() =>var Promise: PromiseConstructorPromise.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 EffectEffect.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: numbertransactionAmount = yield*const fetchTransactionAmount: Effect.Effect<number, never, never>fetchTransactionAmount // Retrieve the discount rate constconst discountRate: numberdiscountRate = yield*const fetchDiscountRate: Effect.Effect<number, never, never>fetchDiscountRate // Calculate discounted amount constconst discountedAmount: numberdiscountedAmount = yield*const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>applyDiscount(const transactionAmount: numbertransactionAmount,const discountRate: numberdiscountRate ) // Apply service charge constconst finalAmount: numberfinalAmount =const addServiceCharge: (amount: number) => numberaddServiceCharge(const discountedAmount: numberdiscountedAmount) // Return the total amount after applying the charge return `Final amount to charge: ${const finalAmount: numberfinalAmount}` }) // Execute the program and log the resultimport EffectEffect.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: Consoleconsole.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) => numberincrement = (x: numberx: number) =>x: numberx + 1 constconst double: (x: number) => numberdouble = (x: numberx: number) =>x: numberx * 2 constconst subtractTen: (x: number) => numbersubtractTen = (x: numberx: number) =>x: numberx - 10 constconst result: numberresult =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) => numberincrement,const double: (x: number) => numberdouble,const subtractTen: (x: number) => numbersubtractTen)var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(const result: numberresult) // Output: 2
這個寫法等同於 subtractTen(double(increment(5))),但可讀性更好。
map
若 input 為 Effect,則可以使用 map 做轉換。
import {function pipe<A>(a: A): A (+19 overloads)pipe,import EffectEffect } from "effect" // Function to add a small service charge to a transaction amount constconst addServiceCharge: (amount: number) => numberaddServiceCharge = (amount: numberamount: number) =>amount: numberamount + 1 // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>fetchTransactionAmount =import EffectEffect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>promise(() =>var Promise: PromiseConstructorPromise.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 EffectEffect.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) => numberaddServiceCharge))import EffectEffect.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: Consoleconsole.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 EffectEffect } from "effect" // Function to apply a discount safely to a transaction amount constconst applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>applyDiscount = (total: numbertotal: number,discountRate: numberdiscountRate: number ):import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<number, Error> =>discountRate: numberdiscountRate === 0 ?import EffectEffect.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 EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(total: numbertotal - (total: numbertotal *discountRate: numberdiscountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>fetchTransactionAmount =import EffectEffect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>promise(() =>var Promise: PromiseConstructorPromise.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 EffectEffect.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: numberamount) =>const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>applyDiscount(amount: numberamount, 5)) )import EffectEffect.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: Consoleconsole.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 的值。
constconst 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 EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(5),import EffectEffect.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 EffectEffect.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: Consoleconsole.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 constconst applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>applyDiscount = (total: numbertotal: number,discountRate: numberdiscountRate: number ):import EffectEffect.interface Effect<out A, out E = never, out R = never>Effect<number, Error> =>discountRate: numberdiscountRate === 0 ?import EffectEffect.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 EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(total: numbertotal - (total: numbertotal *discountRate: numberdiscountRate) / 100) // Simulated asynchronous task to fetch a transaction amount from a database constconst fetchTransactionAmount: Effect.Effect<number, never, never>fetchTransactionAmount =import EffectEffect.const promise: <number>(evaluate: (signal: AbortSignal) => PromiseLike<number>) => Effect.Effect<number, never, never>promise(() =>var Promise: PromiseConstructorPromise.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 EffectEffect.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: numberamount) =>import EffectEffect.const sync: <void>(evaluate: LazyArg<void>) => Effect.Effect<void, never, never>sync(() =>var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log(`Apply a discount to: ${amount: numberamount}`)) ), // `amount` is still available!import EffectEffect.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: numberamount) =>const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>applyDiscount(amount: numberamount, 5)) )import EffectEffect.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: Consoleconsole.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 constwebConfig =const webConfig: Effect.Effect<{ dbConnection: string; port: number; }, never, never>import EffectEffect.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: PromiseConstructorPromise.resolve({PromiseConstructor.resolve<{ dbConnection: string; port: number; }>(value: { dbConnection: string; port: number; }): Promise<{ dbConnection: string; port: number; }> (+2 overloads)dbConnection: stringdbConnection: "localhost",port: numberport: 8080 }) ) // Simulated function to test database connectivity constconst checkDatabaseConnectivity: Effect.Effect<string, never, never>checkDatabaseConnectivity =import EffectEffect.const promise: <string>(evaluate: (signal: AbortSignal) => PromiseLike<string>) => Effect.Effect<string, never, never>promise(() =>var Promise: PromiseConstructorPromise.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 EffectEffect.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 EffectEffect.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: stringdbStatus]) => {var console: Consoleconsole.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)log( `Configuration: ${var JSON: JSONJSON.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: stringdbStatus}` ) }) /* 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> |