Caching Effects
This section covers several functions from the library that help manage caching and memoization in your application.
Memoizing a Function
To memoize an effectful function, create a Cache whose lookup is the function to memoize, then call Cache.get for each input. The cache stores one result per input, so calling the function again with the same input reuses the cached result instead of recomputing it.
Example (Memoizing a Function with Cache)
import { Cache, Effect } from "effect"
let i = 1
// Simulating a task whose result changes on each callconst randomNumber = (n: number) => Effect.sync(() => n + i++)
const program = Effect.gen(function* () { console.log("non-memoized version:") const a = yield* randomNumber(10) // Computes a new result console.log(a) const b = yield* randomNumber(10) // Computes a different result console.log(b)
console.log("memoized version:") const cache = yield* Cache.make({ capacity: Number.MAX_SAFE_INTEGER, lookup: randomNumber, }) const memoized = (n: number) => Cache.get(cache, n) const c = yield* memoized(10) // Computes and caches the result console.log(c) const d = yield* memoized(10) // Reuses the cached result console.log(d)
return { a, b, c, d }})
const result = await Effect.runPromise(program)result // => { a: 11, b: 12, c: 13, d: 13 }once
Ensures an effect is executed only once, even if invoked multiple times.
Example (Single Execution of an Effect)
import { Effect, Console } from "effect"
const program = Effect.gen(function* () { const task1 = Console.log("task1")
// Repeats task1 three times yield* Effect.repeat(task1, { times: 2 })
// Ensures task2 is executed only once const task2 = yield* Effect.cached(Console.log("task2"))
// Attempts to repeat task2, but it will only execute once yield* Effect.repeat(task2, { times: 2 })})
const result = await Effect.runPromise(program)/*Output:task1task1task1task2*/result // => undefinedcached
Returns an effect that computes a result lazily and caches it. Subsequent evaluations of this effect will return the cached result without re-executing the logic.
Example (Lazy Caching of an Expensive Task)
import { Effect, Console } from "effect"
let i = 1
// Simulating an expensive task with a delayconst expensiveTask = Effect.promise<string>(() => { console.log("expensive task...") return new Promise((resolve) => { setTimeout(() => { resolve(`result ${i++}`) }, 100) })})
const program = Effect.gen(function* () { // Without caching, the task is executed each time console.log("-- non-cached version:") yield* expensiveTask.pipe(Effect.andThen(Console.log)) yield* expensiveTask.pipe(Effect.andThen(Console.log))
// With caching, the result is reused after the first run console.log("-- cached version:") const cached = yield* Effect.cached(expensiveTask) yield* cached.pipe(Effect.andThen(Console.log)) yield* cached.pipe(Effect.andThen(Console.log))})
const result = await Effect.runPromise(program)/*Output:-- non-cached version:expensive task...result 1expensive task...result 2-- cached version:expensive task...result 3result 3*/result // => undefinedcachedWithTTL
Returns an effect that caches its result for a specified duration, known as the timeToLive. When the cache expires after the duration, the effect will be recomputed upon next evaluation.
Example (Caching with Time-to-Live)
import { Effect, Console } from "effect"
let i = 1
// Simulating an expensive task with a delayconst expensiveTask = Effect.promise<string>(() => { console.log("expensive task...") return new Promise((resolve) => { setTimeout(() => { resolve(`result ${i++}`) }, 100) })})
const program = Effect.gen(function* () { // Caches the result for 150 milliseconds const cached = yield* Effect.cachedWithTTL(expensiveTask, "150 millis")
// First evaluation triggers the task yield* cached.pipe(Effect.andThen(Console.log))
// Second evaluation returns the cached result yield* cached.pipe(Effect.andThen(Console.log))
// Wait for 200 milliseconds, ensuring the cache expires yield* Effect.sleep("200 millis")
// Recomputes the task after cache expiration yield* cached.pipe(Effect.andThen(Console.log))})
const result = await Effect.runPromise(program)/*Output:expensive task...result 1result 1expensive task...result 2*/result // => undefinedcachedInvalidateWithTTL
Similar to Effect.cachedWithTTL, this function caches an effect’s result for a specified duration. It also includes an additional effect for manually invalidating the cached value before it naturally expires.
Example (Invalidating Cache Manually)
import { Effect, Console } from "effect"
let i = 1
// Simulating an expensive task with a delayconst expensiveTask = Effect.promise<string>(() => { console.log("expensive task...") return new Promise((resolve) => { setTimeout(() => { resolve(`result ${i++}`) }, 100) })})
const program = Effect.gen(function* () { // Caches the result for 150 milliseconds const [cached, invalidate] = yield* Effect.cachedInvalidateWithTTL( expensiveTask, "150 millis", )
// First evaluation triggers the task yield* cached.pipe(Effect.andThen(Console.log))
// Second evaluation returns the cached result yield* cached.pipe(Effect.andThen(Console.log))
// Invalidate the cache before it naturally expires yield* invalidate
// Third evaluation triggers the task again // since the cache was invalidated yield* cached.pipe(Effect.andThen(Console.log))})
const result = await Effect.runPromise(program)/*Output:expensive task...result 1result 1expensive task...result 2*/result // => undefined