diff --git a/function/cache.go b/function/cache.go new file mode 100644 index 0000000..1bb54e0 --- /dev/null +++ b/function/cache.go @@ -0,0 +1,30 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + G "github.com/IBM/fp-go/function/generic" +) + +// Cache converts a unary function into a unary function that caches the value depending on the parameter +func Cache[K comparable, T any](f func(K) T) func(K) T { + return G.Cache(f) +} + +// ContramapCache converts a unary function into a unary function that caches the value depending on the parameter +func ContramapCache[A any, K comparable, T any](kf func(A) K) func(func(A) T) func(A) T { + return G.ContramapCache[func(A) T](kf) +} diff --git a/function/cache_test.go b/function/cache_test.go new file mode 100644 index 0000000..6be6a94 --- /dev/null +++ b/function/cache_test.go @@ -0,0 +1,50 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCache(t *testing.T) { + var count int + + withSideEffect := func(n int) int { + count++ + return n + } + + cached := Cache(withSideEffect) + + assert.Equal(t, 0, count) + + assert.Equal(t, 10, cached(10)) + assert.Equal(t, 1, count) + + assert.Equal(t, 10, cached(10)) + assert.Equal(t, 1, count) + + assert.Equal(t, 20, cached(20)) + assert.Equal(t, 2, count) + + assert.Equal(t, 20, cached(20)) + assert.Equal(t, 2, count) + + assert.Equal(t, 10, cached(10)) + assert.Equal(t, 2, count) +} diff --git a/function/generic/cache.go b/function/generic/cache.go new file mode 100644 index 0000000..778411f --- /dev/null +++ b/function/generic/cache.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package generic + +import ( + "sync" + + L "github.com/IBM/fp-go/internal/lazy" +) + +// Cache converts a unary function into a unary function that caches the value depending on the parameter +func Cache[F ~func(K) T, K comparable, T any](f F) F { + return ContramapCache[F](func(k K) K { return k })(f) +} + +// ContramapCache converts a unary function into a unary function that caches the value depending on the parameter +func ContramapCache[F ~func(A) T, KF func(A) K, A any, K comparable, T any](kf KF) func(F) F { + return CacheCallback[F](kf, getOrCreate[K, T]()) +} + +// getOrCreate is a naive implementation of a cache, without bounds +func getOrCreate[K comparable, T any]() func(K, func() func() T) func() T { + cache := make(map[K]func() T) + var l sync.Mutex + + return func(k K, cb func() func() T) func() T { + // only lock to access a lazy accessor to the value + l.Lock() + existing, ok := cache[k] + if !ok { + existing = cb() + cache[k] = existing + } + l.Unlock() + // compute the value outside of the lock + return existing + } +} + +// CacheCallback converts a unary function into a unary function that caches the value depending on the parameter +func CacheCallback[F ~func(A) T, KF func(A) K, C ~func(K, func() func() T) func() T, A any, K comparable, T any](kf KF, getOrCreate C) func(F) F { + return func(f F) F { + return func(a A) T { + // cache entry + return getOrCreate(kf(a), func() func() T { + return L.Memoize[func() T](func() T { + return f(a) + }) + })() + } + } +} diff --git a/internal/lazy/memoize.go b/internal/lazy/memoize.go new file mode 100644 index 0000000..348ef97 --- /dev/null +++ b/internal/lazy/memoize.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package lazy + +import "sync" + +// Memoize computes the value of the provided IO monad lazily but exactly once +func Memoize[GA ~func() A, A any](ma GA) GA { + // synchronization primitives + var once sync.Once + var result A + // callback + gen := func() { + result = ma() + } + // returns our memoized wrapper + return func() A { + once.Do(gen) + return result + } +} diff --git a/io/generic/io.go b/io/generic/io.go index 2fee2ec..024476b 100644 --- a/io/generic/io.go +++ b/io/generic/io.go @@ -16,11 +16,11 @@ package generic import ( - "sync" "time" F "github.com/IBM/fp-go/function" C "github.com/IBM/fp-go/internal/chain" + L "github.com/IBM/fp-go/internal/lazy" ) // type IO[A any] = func() A @@ -119,18 +119,7 @@ func Flatten[GA ~func() A, GAA ~func() GA, A any](mma GAA) GA { // Memoize computes the value of the provided IO monad lazily but exactly once func Memoize[GA ~func() A, A any](ma GA) GA { - // synchronization primitives - var once sync.Once - var result A - // callback - gen := func() { - result = ma() - } - // returns our memoized wrapper - return func() A { - once.Do(gen) - return result - } + return L.Memoize[GA, A](ma) } // Delay creates an operation that passes in the value after some delay diff --git a/lazy/lazy.go b/lazy/lazy.go index 96f11f4..0f360bb 100644 --- a/lazy/lazy.go +++ b/lazy/lazy.go @@ -83,7 +83,7 @@ func Flatten[A any](mma Lazy[Lazy[A]]) Lazy[A] { return G.Flatten(mma) } -// Memoize computes the value of the provided IO monad lazily but exactly once +// Memoize computes the value of the provided [Lazy] monad lazily but exactly once func Memoize[A any](ma Lazy[A]) Lazy[A] { return G.Memoize(ma) }