已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

缓存 Effect

使用可复用的工具高效管理 Effect 的缓存与记忆化。

本节介绍库中若干用于在应用中管理缓存与记忆化的函数。

cachedFunction

对一个带 effect 的函数进行记忆化,为相同的输入缓存结果,以避免重复计算。

示例(记忆化一个随机数生成器)

import { Effect, Random } from "effect"

const program = Effect.gen(function* () {
  const randomNumber = (n: number) => Random.nextIntBetween(1, n)
  console.log("non-memoized version:")
  console.log(yield* randomNumber(10)) // Generates a new random number
  console.log(yield* randomNumber(10)) // Generates a different number

  console.log("memoized version:")
  const memoized = yield* Effect.cachedFunction(randomNumber)
  console.log(yield* memoized(10)) // Generates and caches the result
  console.log(yield* memoized(10)) // Reuses the cached result
})

Effect.runFork(program)
/*
Example Output:
non-memoized version:
2
8
memoized version:
5
5
*/

once

确保一个 effect 只执行一次,即使它被多次调用也是如此。

示例(Effect 的单次执行)

import { Effect, Console } from "effect"

const program = Effect.gen(function* () {
  const task1 = Console.log("task1")

  // Repeats task1 three times
  yield* Effect.repeatN(task1, 2)

  // Ensures task2 is executed only once
  const task2 = yield* Effect.once(Console.log("task2"))

  // Attempts to repeat task2, but it will only execute once
  yield* Effect.repeatN(task2, 2)
})

Effect.runFork(program)
/*
Output:
task1
task1
task1
task2
*/

cached

返回一个 effect,它会惰性地计算出一个结果并缓存该结果。之后再次对这个 effect 求值时,会直接返回缓存的结果,而不会重新执行其中的逻辑。

示例(惰性缓存一个开销较大的任务)

import { Effect, Console } from "effect"

let i = 1

// Simulating an expensive task with a delay
const 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))
})

Effect.runFork(program)
/*
Output:
-- non-cached version:
expensive task...
result 1
expensive task...
result 2
-- cached version:
expensive task...
result 3
result 3
*/

cachedWithTTL

返回一个 effect,它会把结果缓存指定的时长,这个时长称为 timeToLive。当缓存在这个时长之后过期时,该 effect 会在下一次求值时重新计算。

示例(带存活时间的缓存)

import { Effect, Console } from "effect"

let i = 1

// Simulating an expensive task with a delay
const 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 100 milliseconds, ensuring the cache expires
  yield* Effect.sleep("100 millis")

  // Recomputes the task after cache expiration
  yield* cached.pipe(Effect.andThen(Console.log))
})

Effect.runFork(program)
/*
Output:
expensive task...
result 1
result 1
expensive task...
result 2
*/

cachedInvalidateWithTTL

Effect.cachedWithTTL 类似,这个函数会把一个 effect 的结果缓存指定的时长。它还额外提供一个 effect,用于在缓存自然过期之前手动使其失效。

示例(手动使缓存失效)

import { Effect, Console } from "effect"

let i = 1

// Simulating an expensive task with a delay
const 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))
})

Effect.runFork(program)
/*
Output:
expensive task...
result 1
result 1
expensive task...
result 2
*/