Skip to content

agkloop/go_memoize

Repository files navigation

go_memoize

Workflow Status

go_memoize package provides a set of functions to memoize the results of computations, allowing for efficient caching and retrieval of results based on input parameters. This can significantly improve performance for expensive or frequently called functions.

Features

  • Memoizes functions with TTL, supporting 0 to 7 comparable parameters. List of Memoize Functions
  • Error-aware variants (suffix E) that support compute functions returning (V, error) and do NOT cache when an error is returned.
  • High performance, zero allocation, and zero dependencies.
  • Utilizes the FNV-1a hash algorithm for caching.
  • Thread-safe and concurrent-safe.

Installation

To install the package, use go get:

go get github.com/agkloop/go_memoize

Usage

Basic Memoization

The Memoize function can be used to memoize a function with no parameters:

computeFn := func() int {
    // Expensive computation
    return 42
}

memoizedFn := Memoize(computeFn, 10*time.Second)
result := memoizedFn()

The same for functions with Context:

computeCtxFn := func(ctx context.Context) int {
    // Expensive computation
    return 42
}
memoizedCtxFn := MemoizeCtx(computeCtxFn, 10*time.Second)
result := memoizedCtxFn(context.Background())

Error-aware memoization (do not memoize errors)

If your compute function can fail and returns (V, error), use the E variants. These versions will NOT store a cached value when the compute function returns a non-nil error. This is useful for transient failures where you want the next call to retry the computation rather than returning a cached error result.

Available E variants:

  • MemoizeE (no-arg)
  • Memoize1E .. Memoize7E (1..7 args)
  • MemoizeCtxE and MemoizeCtx1E .. MemoizeCtx7E (context-aware)

Behavior:

  • If a cached value exists for the key, the function returns it and a nil error.
  • If no cached value exists, the compute function is executed.
    • If compute returns (v, nil), v is cached and returned.
    • If compute returns (zeroValue, err) (err != nil), the error is returned and nothing is cached.

Example:

computeFn := func(id int) (string, error) {
    // may return an error sometimes
}

memo := Memoize1E(func(id int) (string, error) { return computeFn(id) }, 30*time.Second)

val, err := memo(123)
if err != nil {
    // transient error; next call will retry since nothing was cached
}

Memoization with Parameters

The package provides functions to memoize functions with up to 7 parameters. Here are some examples:

One Parameter

computeFn := func(a int) int {
    // Expensive computation
    return a * 2
}

memoizedFn := Memoize1(computeFn, 10*time.Second)
result := memoizedFn(5)

The same for functions with Context:

computeCtxFn := func(ctx context.Context, a int) int {
    // Expensive computation
    return a * 2
}

memoizedCtxFn := MemoizeCtx1(computeCtxFn, 10*time.Second)
result := memoizedCtxFn(context.Background(), 5)

Two Parameters

computeFn := func(a int, b string) string {
    // Expensive computation
    return fmt.Sprintf("%d-%s", a, b)
}

memoizedFn := Memoize2(computeFn, 10*time.Second)
result := memoizedFn(5, "example")

The same for functions with Context:

computeCtxFn := func(ctx context.Context, a int, b string) string {
    // Expensive computation
    return fmt.Sprintf("%d-%s", a, b)
}

memoizedCtxFn := MemoizeCtx2(computeCtxFn, 10*time.Second)
result := memoizedCtxFn(context.Background(), 5, "example")

Three Parameters

computeFn := func(a int, b string, c float64) string {
    // Expensive computation
    return fmt.Sprintf("%d-%s-%f", a, b, c)
}

memoizedFn := Memoize3(computeFn, 10*time.Second)
result := memoizedFn(5, "example", 3.14)

The same for functions with Context:

computeCtxFn := func(ctx context.Context, a int, b string, c float64) string {
    // Expensive computation
    return fmt.Sprintf("%d-%s-%f", a, b, c)
}

memoizedCtxFn := MemoizeCtx3(computeCtxFn, 10*time.Second)
result := memoizedCtxFn(context.Background(), 5, "example", 3.14)

Cache Management

The Cache struct is used internally to manage the cached entries. It supports setting, getting, and deleting entries, as well as computing new values if they are not already cached or have expired.

Testing

Unit tests cover the memoization behavior, including the new error-aware variants. To run tests:

# run all tests
go test ./...

# run a specific test
go test ./... -run TestMemoizeE_DoesNotCacheError -v

New tests were added in memoize_error_test.go to verify that error results are not cached and that successful results are cached.

Example

Here is a complete example of using the memoize package:

package main

import (
    "fmt"
    "time"
    m "github.com/agkloop/go_memoize"
)

func main() {
    computeFn := func(a int, b string) string {
        // Simulate an expensive computation
        time.Sleep(2 * time.Second)
        return fmt.Sprintf("%d-%s", a, b)
    }

    memoizedFn := m.Memoize2(computeFn, 10*time.Second)

    // First call will compute the result
    result := memoizedFn(5, "example")
    fmt.Println(result) // Output: 5-example

    // Subsequent calls within 10 seconds will use the cached result
    result = memoizedFn(5, "example")
    fmt.Println(result) // Output: 5-example
}

Functions & Usage Examples

Function Description Example
Memoize Memoizes a function with no params

memoizedFn := Memoize(func() int { return 1 }, time.Minute)
result := memoizedFn()
      
Memoize1 Memoizes a function with 1 param

memoizedFn := Memoize1(func(a int) int { return a * 2 }, time.Minute)
result := memoizedFn(5)
      
Memoize2 Memoizes a function with 2 params

memoizedFn := Memoize2(func(a int, b string) string { return fmt.Sprintf("%d-%s", a, b) }, time.Minute)
result := memoizedFn(5, "example")
      
Memoize3 Memoizes a function with 3 params

memoizedFn := Memoize3(func(a int, b string, c float64) string { return fmt.Sprintf("%d-%s-%f", a, b, c) }, time.Minute)
result := memoizedFn(5, "example", 3.14)
      
Memoize4 Memoizes a function with 4 params

memoizedFn := Memoize4(func(a, b, c, d int) int { return a + b + c + d }, time.Minute)
result := memoizedFn(1, 2, 3, 4)
      
Memoize5 Memoizes a function with 5 params

memoizedFn := Memoize5(func(a, b, c, d, e int) int { return a + b + c + d + e }, time.Minute)
result := memoizedFn(1, 2, 3, 4, 5)
      
Memoize6 Memoizes a function with 6 params

memoizedFn := Memoize6(func(a, b, c, d, e, f int) int { return a + b + c + d + e + f }, time.Minute)
result := memoizedFn(1, 2, 3, 4, 5, 6)
      
Memoize7 Memoizes a function with 7 params

memoizedFn := Memoize7(func(a, b, c, d, e, f, g int) int { return a + b + c + d + e + f + g }, time.Minute)
result := memoizedFn(1, 2, 3, 4, 5, 6, 7)
      
MemoizeE Memoizes a function with no params, error-aware

memoizedFn := MemoizeE(func() (int, error) { return 1, nil }, time.Minute)
result, err := memoizedFn()
      
Memoize1E Memoizes a function with 1 param, error-aware

memoizedFn := Memoize1E(func(a int) (int, error) { return a * 2, nil }, time.Minute)
result, err := memoizedFn(5)
      
Memoize2E Memoizes a function with 2 params, error-aware

memoizedFn := Memoize2E(func(a int, b string) (string, error) { return fmt.Sprintf("%d-%s", a, b), nil }, time.Minute)
result, err := memoizedFn(5, "example")
      
Memoize3E Memoizes a function with 3 params, error-aware

memoizedFn := Memoize3E(func(a int, b string, c float64) (string, error) { return fmt.Sprintf("%d-%s-%f", a, b, c), nil }, time.Minute)
result, err := memoizedFn(5, "example", 3.14)
      
MemoizeCtx Memoizes a function with context and no params

memoizedCtxFn := MemoizeCtx(func(ctx context.Context) int { return 1 }, time.Minute)
result := memoizedCtxFn(context.Background())
      
MemoizeCtx1 Memoizes a function with context and 1 param

memoizedCtxFn := MemoizeCtx1(func(ctx context.Context, a int) int { return a * 2 }, time.Minute)
result := memoizedCtxFn(context.Background(), 5)
      
MemoizeCtx2 Memoizes a function with context and 2 params

memoizedCtxFn := MemoizeCtx2(func(ctx context.Context, a int, b string) string { return fmt.Sprintf("%d-%s", a, b) }, time.Minute)
result := memoizedCtxFn(context.Background(), 5, "example")
      
MemoizeCtx3 Memoizes a function with context and 3 params

memoizedCtxFn := MemoizeCtx3(func(ctx context.Context, a int, b string, c float64) string { return fmt.Sprintf("%d-%s-%f", a, b, c) }, time.Minute)
result := memoizedCtxFn(context.Background(), 5, "example", 3.14)
      
MemoizeCtx4 Memoizes a function with context and 4 params

memoizedCtxFn := MemoizeCtx4(func(ctx context.Context, a, b, c, d int) int { return a + b + c + d }, time.Minute)
result := memoizedCtxFn(context.Background(), 1, 2, 3, 4)
      
MemoizeCtx5 Memoizes a function with context and 5 params

memoizedCtxFn := MemoizeCtx5(func(ctx context.Context, a, b, c, d, e int) int { return a + b + c + d + e }, time.Minute)
result := memoizedCtxFn(context.Background(), 1, 2, 3, 4, 5)
      
MemoizeCtx6 Memoizes a function with context and 6 params

memoizedCtxFn := MemoizeCtx6(func(ctx context.Context, a, b, c, d, e, f int) int { return a + b + c + d + e + f }, time.Minute)
result := memoizedCtxFn(context.Background(), 1, 2, 3, 4, 5, 6)
      
MemoizeCtx7 Memoizes a function with context and 7 params

memoizedCtxFn := MemoizeCtx7(func(ctx context.Context, a, b, c, d, e, f, g int) int { return a + b + c + d + e + f + g }, time.Minute)
result := memoizedCtxFn(context.Background(), 1, 2, 3, 4, 5, 6, 7)
      

Device "Apple M2 Pro"

goos: darwin
goarch: arm64
BenchmarkDo0Mem-10 | 811289566 | 14.77 ns/op | 0 B/op | 0 allocs/op
BenchmarkDo1Mem-10 | 676579908 | 18.26 ns/op | 0 B/op | 0 allocs/op
BenchmarkDo2Mem-10 | 578134332 | 20.99 ns/op | 0 B/op | 0 allocs/op
BenchmarkDo3Mem-10 | 533455237 | 22.67 ns/op | 0 B/op | 0 allocs/op
BenchmarkDo4Mem-10 | 487471639 | 24.73 ns/op | 0 B/op | 0 allocs/op

This project is licensed under the Apache License. See the LICENSE file for details.

About

Golang high performant functional Memoize

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages