From 1b1dccc551df0450184121e18ec6a6964aab22b7 Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Thu, 10 Aug 2023 18:08:11 +0200 Subject: [PATCH] fix: implement FoldMap Signed-off-by: Dr. Carsten Leue --- array/array.go | 10 ++ array/array_test.go | 19 ++++ array/generic/array.go | 17 ++++ array/generic/sort.go | 8 +- array/sort.go | 5 + iterator/stateless/generic/iterator.go | 13 +++ iterator/stateless/iterator.go | 11 +++ iterator/stateless/iterator_test.go | 13 +++ record/generic/record.go | 128 +++++++++++++++++++++++++ record/record.go | 51 ++++++++++ record/record_test.go | 32 +++++++ 11 files changed, 306 insertions(+), 1 deletion(-) diff --git a/array/array.go b/array/array.go index 1d2bc68..029d8ae 100644 --- a/array/array.go +++ b/array/array.go @@ -294,3 +294,13 @@ func SliceRight[A any](start int) func([]A) []A { func Copy[A any](b []A) []A { return G.Copy(b) } + +// FoldMap maps and folds an array. Map the Array passing each value to the iterating function. Then fold the results using the provided Monoid. +func FoldMap[A, B any](m M.Monoid[B]) func(func(A) B) func([]A) B { + return G.FoldMap[[]A](m) +} + +// Fold folds the array using the provided Monoid. +func Fold[A any](m M.Monoid[A]) func([]A) A { + return G.Fold[[]A](m) +} diff --git a/array/array_test.go b/array/array_test.go index 57fb20b..0b12293 100644 --- a/array/array_test.go +++ b/array/array_test.go @@ -173,3 +173,22 @@ func TestFilterMap(t *testing.T) { assert.Equal(t, From("a1", "a3"), res) } + +func TestFoldMap(t *testing.T) { + src := From("a", "b", "c") + + fold := FoldMap[string](S.Monoid)(strings.ToUpper) + + assert.Equal(t, "ABC", fold(src)) +} + +func ExampleFoldMap() { + src := From("a", "b", "c") + + fold := FoldMap[string](S.Monoid)(strings.ToUpper) + + fmt.Println(fold(src)) + + // Output: ABC + +} diff --git a/array/generic/array.go b/array/generic/array.go index 6452440..e5abb64 100644 --- a/array/generic/array.go +++ b/array/generic/array.go @@ -18,6 +18,7 @@ package generic import ( F "github.com/IBM/fp-go/function" "github.com/IBM/fp-go/internal/array" + M "github.com/IBM/fp-go/monoid" O "github.com/IBM/fp-go/option" "github.com/IBM/fp-go/tuple" ) @@ -209,3 +210,19 @@ func Copy[AS ~[]A, A any](b AS) AS { copy(buf, b) return buf } + +func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B { + return func(f func(A) B) func(AS) B { + return func(as AS) B { + return array.Reduce(as, func(cur B, a A) B { + return m.Concat(cur, f(a)) + }, m.Empty()) + } + } +} + +func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A { + return func(as AS) A { + return array.Reduce(as, m.Concat, m.Empty()) + } +} diff --git a/array/generic/sort.go b/array/generic/sort.go index c248966..7a4026b 100644 --- a/array/generic/sort.go +++ b/array/generic/sort.go @@ -18,11 +18,17 @@ package generic import ( "sort" + F "github.com/IBM/fp-go/function" O "github.com/IBM/fp-go/ord" ) // Sort implements a stable sort on the array given the provided ordering func Sort[GA ~[]T, T any](ord O.Ord[T]) func(ma GA) GA { + return SortByKey[GA](ord, F.Identity[T]) +} + +// SortByKey implements a stable sort on the array given the provided ordering on an extracted key +func SortByKey[GA ~[]T, K, T any](ord O.Ord[K], f func(T) K) func(ma GA) GA { return func(ma GA) GA { // nothing to sort @@ -34,7 +40,7 @@ func Sort[GA ~[]T, T any](ord O.Ord[T]) func(ma GA) GA { cpy := make(GA, l) copy(cpy, ma) sort.Slice(cpy, func(i, j int) bool { - return ord.Compare(cpy[i], cpy[j]) < 0 + return ord.Compare(f(cpy[i]), f(cpy[j])) < 0 }) return cpy } diff --git a/array/sort.go b/array/sort.go index 28138eb..302ac38 100644 --- a/array/sort.go +++ b/array/sort.go @@ -24,3 +24,8 @@ import ( func Sort[T any](ord O.Ord[T]) func(ma []T) []T { return G.Sort[[]T](ord) } + +// SortByKey implements a stable sort on the array given the provided ordering on an extracted key +func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T { + return G.SortByKey[[]T](ord, f) +} diff --git a/iterator/stateless/generic/iterator.go b/iterator/stateless/generic/iterator.go index c3e6f9f..154ea76 100644 --- a/iterator/stateless/generic/iterator.go +++ b/iterator/stateless/generic/iterator.go @@ -20,6 +20,7 @@ import ( F "github.com/IBM/fp-go/function" "github.com/IBM/fp-go/internal/utils" IO "github.com/IBM/fp-go/iooption/generic" + M "github.com/IBM/fp-go/monoid" N "github.com/IBM/fp-go/number" O "github.com/IBM/fp-go/option" T "github.com/IBM/fp-go/tuple" @@ -226,3 +227,15 @@ func FilterChain[GVV ~func() O.Option[T.Tuple2[GVV, GV]], GV ~func() O.Option[T. Flatten[GVV], ) } + +func FoldMap[GU ~func() O.Option[T.Tuple2[GU, U]], FCT ~func(U) V, U, V any](m M.Monoid[V]) func(FCT) func(ma GU) V { + return func(f FCT) func(ma GU) V { + return Reduce[GU](func(cur V, a U) V { + return m.Concat(cur, f(a)) + }, m.Empty()) + } +} + +func Fold[GU ~func() O.Option[T.Tuple2[GU, U]], U any](m M.Monoid[U]) func(ma GU) U { + return Reduce[GU](m.Concat, m.Empty()) +} diff --git a/iterator/stateless/iterator.go b/iterator/stateless/iterator.go index 76a8a4d..c08d4d8 100644 --- a/iterator/stateless/iterator.go +++ b/iterator/stateless/iterator.go @@ -18,6 +18,7 @@ package stateless import ( G "github.com/IBM/fp-go/iterator/stateless/generic" L "github.com/IBM/fp-go/lazy" + M "github.com/IBM/fp-go/monoid" O "github.com/IBM/fp-go/option" T "github.com/IBM/fp-go/tuple" ) @@ -123,3 +124,13 @@ func Count(start int) Iterator[int] { func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) func(ma Iterator[U]) Iterator[V] { return G.FilterChain[Iterator[Iterator[V]], Iterator[V], Iterator[U]](f) } + +// FoldMap maps and folds an iterator. Map the iterator passing each value to the iterating function. Then fold the results using the provided Monoid. +func FoldMap[U, V any](m M.Monoid[V]) func(func(U) V) func(ma Iterator[U]) V { + return G.FoldMap[Iterator[U], func(U) V, U, V](m) +} + +// Fold folds the iterator using the provided Monoid. +func Fold[U any](m M.Monoid[U]) func(Iterator[U]) U { + return G.Fold[Iterator[U]](m) +} diff --git a/iterator/stateless/iterator_test.go b/iterator/stateless/iterator_test.go index 3eee813..d6ebd47 100644 --- a/iterator/stateless/iterator_test.go +++ b/iterator/stateless/iterator_test.go @@ -18,12 +18,14 @@ package stateless import ( "fmt" "math" + "strings" "testing" A "github.com/IBM/fp-go/array" F "github.com/IBM/fp-go/function" "github.com/IBM/fp-go/internal/utils" O "github.com/IBM/fp-go/option" + S "github.com/IBM/fp-go/string" "github.com/stretchr/testify/assert" ) @@ -102,3 +104,14 @@ func TestAp(t *testing.T) { assert.Equal(t, A.From("a-1-c", "a-1-d", "a-2-c", "a-2-d", "b-1-c", "b-1-d", "b-2-c", "b-2-d"), it) } + +func ExampleFoldMap() { + src := From("a", "b", "c") + + fold := FoldMap[string](S.Monoid)(strings.ToUpper) + + fmt.Println(fold(src)) + + // Output: ABC + +} diff --git a/record/generic/record.go b/record/generic/record.go index cc1914c..5c13b9b 100644 --- a/record/generic/record.go +++ b/record/generic/record.go @@ -16,11 +16,14 @@ package generic import ( + "sort" + F "github.com/IBM/fp-go/function" G "github.com/IBM/fp-go/internal/record" Mg "github.com/IBM/fp-go/magma" Mo "github.com/IBM/fp-go/monoid" O "github.com/IBM/fp-go/option" + "github.com/IBM/fp-go/ord" T "github.com/IBM/fp-go/tuple" ) @@ -40,6 +43,46 @@ func Values[M ~map[K]V, GV ~[]V, K comparable, V any](r M) GV { return collect[M, GV](r, F.Second[K, V]) } +func KeysOrd[M ~map[K]V, GK ~[]K, K comparable, V any](o ord.Ord[K]) func(r M) GK { + return func(r M) GK { + return collectOrd[M, GK](o, r, F.First[K, V]) + } +} + +func ValuesOrd[M ~map[K]V, GV ~[]V, K comparable, V any](o ord.Ord[K]) func(r M) GV { + return func(r M) GV { + return collectOrd[M, GV](o, r, F.Second[K, V]) + } +} + +func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M, f func(K, V) R) GR { + // create the entries + entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r) + // collect this array + ft := T.Tupled2(f) + count := len(entries) + result := make(GR, count) + for i := count - 1; i >= 0; i-- { + result[i] = ft(entries[i]) + } + // done + return result +} + +func reduceOrd[M ~map[K]V, K comparable, V, R any](o ord.Ord[K], r M, f func(K, R, V) R, initial R) R { + // create the entries + entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r) + // collect this array + current := initial + count := len(entries) + for i := 0; i < count; i++ { + t := entries[i] + current = f(T.First(t), current, T.Second(t)) + } + // done + return current +} + func collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](r M, f func(K, V) R) GR { count := len(r) result := make(GR, count) @@ -250,6 +293,27 @@ func ToArray[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT { return collect[M, GT](r, T.MakeTuple2[K, V]) } +func toEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K], r M) GT { + // total number of elements + count := len(r) + // produce an array that we can sort by key + entries := make(GT, count) + idx := 0 + for k, v := range r { + entries[idx] = T.MakeTuple2(k, v) + idx++ + } + sort.Slice(entries, func(i, j int) bool { + return o.Compare(T.First(entries[i]), T.First(entries[j])) < 0 + }) + // final entries + return entries +} + +func ToEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K]) func(r M) GT { + return F.Bind1st(toEntriesOrd[M, GT, K, V], o) +} + func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT { return ToArray[M, GT](r) } @@ -379,3 +443,67 @@ func IsNonNil[M ~map[K]V, K comparable, V any](m M) bool { func ConstNil[M ~map[K]V, K comparable, V any]() M { return (M)(nil) } + +func FoldMap[AS ~map[K]A, K comparable, A, B any](m Mo.Monoid[B]) func(func(A) B) func(AS) B { + return func(f func(A) B) func(AS) B { + return Reduce[AS](func(cur B, a A) B { + return m.Concat(cur, f(a)) + }, m.Empty()) + } +} + +func Fold[AS ~map[K]A, K comparable, A any](m Mo.Monoid[A]) func(AS) A { + return Reduce[AS](m.Concat, m.Empty()) +} + +func FoldMapWithIndex[AS ~map[K]A, K comparable, A, B any](m Mo.Monoid[B]) func(func(K, A) B) func(AS) B { + return func(f func(K, A) B) func(AS) B { + return ReduceWithIndex[AS](func(k K, cur B, a A) B { + return m.Concat(cur, f(k, a)) + }, m.Empty()) + } +} + +func ReduceOrdWithIndex[M ~map[K]V, K comparable, V, R any](o ord.Ord[K]) func(func(K, R, V) R, R) func(M) R { + return func(f func(K, R, V) R, initial R) func(M) R { + return func(m M) R { + return reduceOrd(o, m, f, initial) + } + } +} + +func ReduceOrd[M ~map[K]V, K comparable, V, R any](o ord.Ord[K]) func(func(R, V) R, R) func(M) R { + ro := ReduceOrdWithIndex[M, K, V, R](o) + return func(f func(R, V) R, initial R) func(M) R { + return ro(F.Ignore1of3[K](f), initial) + } +} + +func FoldMapOrd[AS ~map[K]A, K comparable, A, B any](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(A) B) func(AS) B { + red := ReduceOrd[AS, K, A, B](o) + return func(m Mo.Monoid[B]) func(func(A) B) func(AS) B { + return func(f func(A) B) func(AS) B { + return red(func(cur B, a A) B { + return m.Concat(cur, f(a)) + }, m.Empty()) + } + } +} + +func FoldOrd[AS ~map[K]A, K comparable, A any](o ord.Ord[K]) func(m Mo.Monoid[A]) func(AS) A { + red := ReduceOrd[AS, K, A, A](o) + return func(m Mo.Monoid[A]) func(AS) A { + return red(m.Concat, m.Empty()) + } +} + +func FoldMapOrdWithIndex[AS ~map[K]A, K comparable, A, B any](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(K, A) B) func(AS) B { + red := ReduceOrdWithIndex[AS, K, A, B](o) + return func(m Mo.Monoid[B]) func(func(K, A) B) func(AS) B { + return func(f func(K, A) B) func(AS) B { + return red(func(k K, cur B, a A) B { + return m.Concat(cur, f(k, a)) + }, m.Empty()) + } + } +} diff --git a/record/record.go b/record/record.go index 7cc278f..7354d04 100644 --- a/record/record.go +++ b/record/record.go @@ -19,6 +19,7 @@ import ( Mg "github.com/IBM/fp-go/magma" Mo "github.com/IBM/fp-go/monoid" O "github.com/IBM/fp-go/option" + "github.com/IBM/fp-go/ord" G "github.com/IBM/fp-go/record/generic" T "github.com/IBM/fp-go/tuple" ) @@ -215,3 +216,53 @@ func FilterChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) f func FilterChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 { return G.FilterChain[map[K]V1](m) } + +// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid. +func FoldMap[K comparable, A, B any](m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B { + return G.FoldMap[map[K]A](m) +} + +// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid. +func FoldMapWithIndex[K comparable, A, B any](m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B { + return G.FoldMapWithIndex[map[K]A](m) +} + +// Fold folds the record using the provided Monoid. +func Fold[K comparable, A any](m Mo.Monoid[A]) func(map[K]A) A { + return G.Fold[map[K]A](m) +} + +// ReduceOrdWithIndex reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order +func ReduceOrdWithIndex[V, R any, K comparable](o ord.Ord[K]) func(func(K, R, V) R, R) func(map[K]V) R { + return G.ReduceOrdWithIndex[map[K]V, K, V, R](o) +} + +// ReduceOrd reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order +func ReduceOrd[V, R any, K comparable](o ord.Ord[K]) func(func(R, V) R, R) func(map[K]V) R { + return G.ReduceOrd[map[K]V, K, V, R](o) +} + +// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order +func FoldMapOrd[A, B any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B { + return G.FoldMapOrd[map[K]A, K, A, B](o) +} + +// Fold folds the record using the provided Monoid with the items passed in the given order +func FoldOrd[A any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[A]) func(map[K]A) A { + return G.FoldOrd[map[K]A, K, A](o) +} + +// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order +func FoldMapOrdWithIndex[K comparable, A, B any](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B { + return G.FoldMapOrdWithIndex[map[K]A, K, A, B](o) +} + +// KeysOrd returns the keys in the map in their given order +func KeysOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []K { + return G.KeysOrd[map[K]V, []K, K, V](o) +} + +// ValuesOrd returns the values in the map ordered by their keys in the given order +func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []V { + return G.ValuesOrd[map[K]V, []V, K, V](o) +} diff --git a/record/record_test.go b/record/record_test.go index 78610ca..baabe95 100644 --- a/record/record_test.go +++ b/record/record_test.go @@ -18,10 +18,12 @@ package record import ( "fmt" "sort" + "strings" "testing" "github.com/IBM/fp-go/internal/utils" O "github.com/IBM/fp-go/option" + S "github.com/IBM/fp-go/string" "github.com/stretchr/testify/assert" ) @@ -99,3 +101,33 @@ func TestFilterChain(t *testing.T) { "c": "c3", }, res) } + +func ExampleFoldMap() { + src := map[string]string{ + "a": "a", + "b": "b", + "c": "c", + } + + fold := FoldMap[string, string](S.Monoid)(strings.ToUpper) + + fmt.Println(fold(src)) + + // Output: ABC + +} + +func ExampleValuesOrd() { + src := map[string]string{ + "c": "a", + "b": "b", + "a": "c", + } + + getValues := ValuesOrd[string](S.Ord) + + fmt.Println(getValues(src)) + + // Output: [c b a] + +}