Skip to content

Commit

Permalink
feat: Adds a way to use generics with this library.
Browse files Browse the repository at this point in the history
  • Loading branch information
jfyne committed Jan 12, 2023
1 parent 0b5d6a3 commit c2dc1e3
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 0 deletions.
38 changes: 38 additions & 0 deletions memoize.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package memoize

import (
"errors"
"time"

"github.com/patrickmn/go-cache"
Expand Down Expand Up @@ -47,3 +48,40 @@ func (m *Memoizer) Memoize(key string, fn func() (interface{}, error)) (interfac
})
return value, err, false
}

// ErrMismatchedType if data returned from the cache does not match the expected type.
var ErrMismatchedType = errors.New("data returned does not match expected type")

// MemoizedFunction the expensive function to be called.
type MemoizedFunction[T any] func() (T, error)

// Call executes and returns the results of the given function, unless there was a cached value of the same key.
// Only one execution is in-flight for a given key at a time.
// The boolean return value indicates whether v was previously stored.
func Call[T any](m *Memoizer, key string, fn MemoizedFunction[T]) (T, error, bool) {
// Check cache
value, found := m.Storage.Get(key)
if found {
v, ok := value.(T)
if !ok {
return v, ErrMismatchedType, true
}
return v, nil, true
}

// Combine memoized function with a cache store
value, err, _ := m.group.Do(key, func() (any, error) {
data, innerErr := fn()

if innerErr == nil {
m.Storage.Set(key, data, cache.DefaultExpiration)
}

return data, innerErr
})
v, ok := value.(T)
if !ok {
return v, ErrMismatchedType, false
}
return v, err, false
}
69 changes: 69 additions & 0 deletions memoize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,72 @@ func (t *F) TestFailure() {
t.So(result.(int), ShouldEqual, 2)
t.So(cached, ShouldBeTrue)
}

// TestBasicGenerics adopts the code from readme.md into a simple test case
// but using generics.
func (t *F) TestBasicGenerics() {
expensiveCalls := 0

// Function tracks how many times its been called
expensive := func() (int, error) {
expensiveCalls++
return expensiveCalls, nil
}

cache := NewMemoizer(90*time.Second, 10*time.Minute)

// First call SHOULD NOT be cached
result, err, cached := Call(cache, "key1", expensive)
t.So(err, ShouldBeNil)
t.So(result, ShouldEqual, 1)
t.So(cached, ShouldBeFalse)

// Second call on same key SHOULD be cached
result, err, cached = Call(cache, "key1", expensive)
t.So(err, ShouldBeNil)
t.So(result, ShouldEqual, 1)
t.So(cached, ShouldBeTrue)

// First call on a new key SHOULD NOT be cached
result, err, cached = Call(cache, "key2", expensive)
t.So(err, ShouldBeNil)
t.So(result, ShouldEqual, 2)
t.So(cached, ShouldBeFalse)
}

// TestFailureGenerics checks that failed function values are not cached
// when using generics.
func (t *F) TestFailureGenerics() {
calls := 0

// This function will fail IFF it has not been called before.
twoForTheMoney := func() (int, error) {
calls++

if calls == 1 {
return calls, errors.New("Try again")
} else {
return calls, nil
}
}

cache := NewMemoizer(90*time.Second, 10*time.Minute)

// First call should fail, and not be cached
result, err, cached := Call(cache, "key1", twoForTheMoney)
t.So(err, ShouldNotBeNil)
t.So(result, ShouldEqual, 1)
t.So(cached, ShouldBeFalse)

// Second call should succeed, and not be cached
result, err, cached = Call(cache, "key1", twoForTheMoney)
t.So(err, ShouldBeNil)
t.So(result, ShouldEqual, 2)
t.So(cached, ShouldBeFalse)

// Third call should succeed, and be cached
result, err, cached = Call(cache, "key1", twoForTheMoney)
t.So(err, ShouldBeNil)
t.So(result, ShouldEqual, 2)
t.So(cached, ShouldBeTrue)
}
30 changes: 30 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,33 @@ In the example above, `result` is:
All the hard stuff is punted to patrickmn's [go-cache](https://github.com/patrickmn/go-cache) and the Go team's [x/sync/singleflight](https://github.com/golang/sync), I just lashed them together.

Also note that `cache.Storage` is exported, so you can use the underlying cache features - such as [Flush](https://godoc.org/github.com/patrickmn/go-cache#Cache.Flush) or [SaveFile](https://godoc.org/github.com/patrickmn/go-cache#Cache.SaveFile).

### Generics

You can also use generics. The same example as above

```golang
import (
"time"

"github.com/kofalt/go-memoize"
)

// Any expensive call that you wish to cache
expensive := func() (string, error) {
time.Sleep(3 * time.Second)
return "some data", nil
}

// Cache expensive calls in memory for 90 seconds, purging old entries every 10 minutes.
cache := memoize.NewMemoizer(90*time.Second, 10*time.Minute)

// This will call the expensive func, and return a string.
result, err, cached := memoize.Call(cache, "key1", expensive)

// This will be cached
result, err, cached = memoize.Call(cache, "key1", expensive)

// This uses a new cache key, so expensive is called again
result, err, cached = memoize.Call(cache, "key2", expensive)
```

0 comments on commit c2dc1e3

Please sign in to comment.