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 }cached
Effect.cached creates a reusable effect that executes the original effect at most once and caches its outcome.
First, obtain the cached effect with const cached = yield* Effect.cached(task). This step does not execute task. The first execution of cached runs task. Further executions behave as follows:
| State of the first execution | Behavior of further executions of cached |
|---|---|
Succeeded with 42 | Return the cached value 42 without running task again. |
| Failed | Fail with the cached failure without retrying task. |
| Interrupted | Propagate the cached interruption without running task again. |
| Still running | Wait for it to complete and share the same outcome. |
Each execution of Effect.cached(task) creates an independent cache, so create the cached effect once and reuse it.
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