筆記 - Effect Essentials

14 min read

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.

Effect – The best way to build robust apps in TypeScript

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 個部分:

  1. Success - 成功時返回值的型別。為 void 時代表沒有返回值,為 never 時代表會一直執行或者直到失敗。
  2. Error - 失敗時的錯誤型別。為 never 時代表沒有不會有任何錯誤發生。
  3. Requirements - 執行時時所需要的 Context 或 依賴資料 的型別。為 never 時代表沒有任何依賴。

Requirements 的資料會存在在 EffectContext 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) => numberdivide = (a: numbera: number, b: numberb: number): number => { if (b: numberb === 0) { throw new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Cannot divide by zero')
} return a: numbera / b: numberb }

無法在 function 明確表示可能會出現的 Errors。

在 Effect 可以使用 Effect.failEffect.succeed 讓 function 有更明確的描述。

Succeed

const
const success: Effect.Effect<number, never, never>
success
= import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(8964)
  • 總是成功:成功時返回一個 number 型別的值
  • 不會失敗
  • 無需依賴

Fail

const
const failure: Effect.Effect<never, Error, never>
failure
= import EffectEffect.const fail: <Error>(error: Error) => Effect.Effect<never, Error, never>fail(new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Something went wrong'))
  • 不會成功
  • 可能失敗:執行時拋出的錯誤為 Error 型別
  • 無需依賴

Example with Succeed and Fail

const const 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(new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
('Cannot divide by zero'))
: import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(a: numbera / b: numberb) const
const 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 實現延遲的計算。

note

thunks 是一個不接受任何參數的函數並可能返回一些值。

為了模擬同步效果,Effect 提供了 synctry 並接受一個 thunk。

sync

const const 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!' }) const
const program: Effect.Effect<string, never, never>
program
= const log: (message: string) => Effect.Effect<string, never, never>log('Hello, World!')
  • 成功時返回 string 型別
  • 不會失敗
  • 無需依賴
warning

sync 不應該拋出任何錯誤,若需要則應考慮使用 try

try

const const parse: (input: string) => Effect.Effect<any, Error, never>parse = (input: stringinput: string) => import EffectEffect.
try<any, Error>(options: { readonly try: LazyArg<any>; readonly catch: (error: unknown) => Error; }): Effect.Effect<any, Error, never> (+1 overload) export try
try
({
try: 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) => new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(`something went wrong ${unknown: unknownunknown}`)
}) const
const program: Effect.Effect<any, Error, never>
program
= const parse: (input: string) => Effect.Effect<any, Error, never>parse('')
  • 成功時返回 JSON.parse 的型別 any
  • 失敗時返回 Error 型別
  • 無需依賴

Modeling Asynchronous Effects

promisetryPromise 一樣接收一個 thunk 但返回值是 Promise

promise

const const 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>( () => new
var Promise: PromiseConstructor new <string>(executor: (resolve: (value: string | PromiseLike<string>) => void, reject: (reason?: any) => void) => void) => Promise<string>
Promise
((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) }) ) const
const program: Effect.Effect<string, never, never>
program
= const delay: (message: string) => Effect.Effect<string, never, never>delay('Async operation completed successfully!')
  • 成功時返回 string 型別
  • 不會失敗
  • 無需依賴
warning

promise 不應該拋出任何錯誤,若需要則應考慮使用 tryPromise

tryPromise

const const getTodo: (id: number) => Effect.Effect<Response, Error, never>getTodo = (id: numberid: number) => import EffectEffect.
const tryPromise: <Response, Error>(options: { readonly try: (signal: AbortSignal) => PromiseLike<Response>; readonly catch: (error: unknown) => Error; }) => Effect.Effect<...> (+1 overload)
tryPromise
({
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) => new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
(`something went wrong ${unknown: unknownunknown}`)
}) const
const 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: 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)) } }) }) const
const 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: numberi = 0 const const badEffect: Effect.Effect<number, never, never>badEffect = import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(let i: numberi++) const const 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: 0 import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const badEffect: Effect.Effect<number, never, never>badEffect) // Output: 0 import EffectEffect.const runSync: <number, never>(effect: Effect.Effect<number, never, never>) => numberrunSync(const goodEffect: Effect.Effect<number, never, never>goodEffect) // Output: 1 import 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 === const const 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.
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)
zipWith
(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 const const 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.
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)
zipWith
(
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 === const const 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(new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Cannot divide by zero"))
: import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(a: numbera / b: numberb) const const 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(new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Cannot divide by zero"))
: import EffectEffect.const succeed: <number>(value: number) => Effect.Effect<number, never, never>succeed(a: numbera / b: numberb) )

Cheatsheet

FunctionGivenTo
succeedAEffect<A>
failEEffect<never, E>
sync() => AEffect<A>
try() => AEffect<A, UnknownException>
try (overload)() => A, unknown => EEffect<A, E>
promise() => Promise<A>Effect<A>
tryPromise() => Promise<A>Effect<A, UnknownException>
tryPromise (overload)() => Promise<A>, unknown => EEffect<A, E>
async(Effect<A, E> => void) => voidEffect<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 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 }) const const 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

const const 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 } */ const const 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.
const runPromise: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
runPromise
(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: 1
import EffectEffect.
const runPromise: <never, string>(effect: Effect.Effect<never, string, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<never>
runPromise
(import EffectEffect.const fail: <string>(error: string) => Effect.Effect<never, string, never>fail("my error")) // rejects

runPromiseExit

runPromise 一樣但返回的值為 Exit

import EffectEffect.
const runPromiseExit: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<Exit<number, never>>
runPromiseExit
(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.
const runPromiseExit: <never, string>(effect: Effect.Effect<never, string, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<Exit<never, string>>
runPromiseExit
(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" const const 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") ) const const 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 const const addServiceCharge: (amount: number) => numberaddServiceCharge = (amount: numberamount: number) => amount: numberamount + 1 // Function to apply a discount safely to a transaction amount const const 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(new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Discount rate cannot be zero"))
: 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 const const 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 const const 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 const const 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 const const transactionAmount: numbertransactionAmount = yield* const fetchTransactionAmount: Effect.Effect<number, never, never>fetchTransactionAmount // Retrieve the discount rate const const discountRate: numberdiscountRate = yield* const fetchDiscountRate: Effect.Effect<number, never, never>fetchDiscountRate // Calculate discounted amount const const discountedAmount: numberdiscountedAmount = yield* const applyDiscount: (total: number, discountRate: number) => Effect.Effect<number, Error>applyDiscount( const transactionAmount: numbertransactionAmount, const discountRate: numberdiscountRate ) // Apply service charge const const 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 result import EffectEffect.
const runPromise: <string, Error>(effect: Effect.Effect<string, Error, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<string>
runPromise
(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" const const increment: (x: number) => numberincrement = (x: numberx: number) => x: numberx + 1 const const double: (x: number) => numberdouble = (x: numberx: number) => x: numberx * 2 const const subtractTen: (x: number) => numbersubtractTen = (x: numberx: number) => x: numberx - 10 const const 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 const const addServiceCharge: (amount: number) => numberaddServiceCharge = (amount: numberamount: number) => amount: numberamount + 1 // Simulated asynchronous task to fetch a transaction amount from a database const const 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 const const 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.
const runPromise: <number, never>(effect: Effect.Effect<number, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
runPromise
(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 const const 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(new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Discount rate cannot be zero"))
: 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 const const 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)) const const 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.
const runPromise: <number, Error>(effect: Effect.Effect<number, Error, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
runPromise
(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 的值。

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 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.
const runPromise: <string, never>(effect: Effect.Effect<string, never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<string>
runPromise
(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, flatMapas 的組合,

const transformedEffect = pipe(myEffect, Effect.andThen(anotherEffect)) const transformedEffect = Effect.andThen(myEffect, anotherEffect) const transformedEffect = myEffect.pipe(Effect.andThen(anotherEffect))

andThen 有 6 種 overload:

  1. 一個值 (as)
  2. function 返回一個值 (map)
  3. 一個 Promise
  4. function 返回一個 Promise
  5. 一個 Effect
  6. 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: 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(new
var Error: ErrorConstructor new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error
("Discount rate cannot be zero"))
: 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 const const 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)) const const 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.
const runPromise: <number, Error>(effect: Effect.Effect<number, Error, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<number>
runPromise
(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 const
const webConfig: Effect.Effect<{ dbConnection: string; port: number; }, never, never>
webConfig
= import EffectEffect.
const promise: <{ dbConnection: string; port: number; }>(evaluate: (signal: AbortSignal) => PromiseLike<{ dbConnection: string; port: number; }>) => Effect.Effect<{ dbConnection: string; port: number; }, never, never>
promise
(() =>
var Promise: PromiseConstructorPromise.
PromiseConstructor.resolve<{ dbConnection: string; port: number; }>(value: { dbConnection: string; port: number; }): Promise<{ dbConnection: string; port: number; }> (+2 overloads)
resolve
({ dbConnection: stringdbConnection: "localhost", port: numberport: 8080 })
) // Simulated function to test database connectivity const const 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 const
const startupChecks: Effect.Effect<[{ dbConnection: string; port: number; }, string], never, never>
startupChecks
= import EffectEffect.
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<...>
all
([
const webConfig: Effect.Effect<{ dbConnection: string; port: number; }, never, never>
webConfig
, const checkDatabaseConnectivity: Effect.Effect<string, never, never>checkDatabaseConnectivity])
import EffectEffect.
const runPromise: <[{ dbConnection: string; port: number; }, string], never>(effect: Effect.Effect<[{ dbConnection: string; port: number; }, string], never, never>, options?: { readonly signal?: AbortSignal; } | undefined) => Promise<...>
runPromise
(
const startupChecks: Effect.Effect<[{ dbConnection: string; port: number; }, string], never, never>
startupChecks
).
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<...>
then
(([
config: { dbConnection: string; port: number; }
config
, 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: { dbConnection: string; port: number; }
config
)}, DB Status: ${dbStatus: stringdbStatus}`
) }) /* Output: Configuration: {"dbConnection":"localhost","port":8080}, DB Status: Connected to Database */

Cheatsheet

FunctionInputOutput
mapEffect<A, E, R>, A => BEffect<B, E, R>
flatMapEffect<A, E, R>, A => Effect<B, E, R>Effect<B, E, R>
andThenEffect<A, E, R>, *Effect<B, E, R>
tapEffect<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>