From c0028918ae0c8774cb7efb42c0eea7e9033cd3e5 Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Thu, 10 Aug 2023 11:46:30 +0200 Subject: [PATCH] fix: implement FilterChain Signed-off-by: Dr. Carsten Leue --- array/array.go | 8 +- array/array_test.go | 31 +++++++ array/generic/array.go | 11 +++ endomorphism/monoid.go | 8 +- iterator/stateless/generic/iterator.go | 7 ++ iterator/stateless/iterator.go | 5 ++ record/doc.go | 17 ++++ record/generic/monoid.go | 15 ++++ record/generic/record.go | 112 +++++++++++++++++++++++-- record/generic/semigroup.go | 15 +++- record/monoid.go | 16 ++++ record/record.go | 47 +++++++++++ record/record_test.go | 28 +++++++ record/semigroup.go | 8 ++ 14 files changed, 311 insertions(+), 17 deletions(-) create mode 100644 record/doc.go diff --git a/array/array.go b/array/array.go index 3ac7f07..1d2bc68 100644 --- a/array/array.go +++ b/array/array.go @@ -108,10 +108,16 @@ func MonadFilterMap[A, B any](fa []A, f func(a A) O.Option[B]) []B { return G.MonadFilterMap[[]A, []B](fa, f) } +// FilterChain maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones. func FilterMap[A, B any](f func(a A) O.Option[B]) func([]A) []B { return G.FilterMap[[]A, []B](f) } +// FilterChain maps an array with an iterating function that returns an [O.Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result. +func FilterChain[A, B any](f func(A) O.Option[[]B]) func([]A) []B { + return G.FilterChain[[]A](f) +} + func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B { return func(fa []A) []B { return filterMapRef(fa, pred, f) @@ -237,7 +243,7 @@ func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A { } func Flatten[A any](mma [][]A) []A { - return MonadChain(mma, F.Identity[[]A]) + return G.Flatten(mma) } func Slice[A any](low, high int) func(as []A) []A { diff --git a/array/array_test.go b/array/array_test.go index 82a4a1a..57fb20b 100644 --- a/array/array_test.go +++ b/array/array_test.go @@ -16,6 +16,7 @@ package array import ( + "fmt" "strings" "testing" @@ -142,3 +143,33 @@ func TestPartition(t *testing.T) { assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]())) assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3))) } + +func TestFilterChain(t *testing.T) { + src := From(1, 2, 3) + + f := func(i int) O.Option[[]string] { + if i%2 != 0 { + return O.Of(From(fmt.Sprintf("a%d", i), fmt.Sprintf("b%d", i))) + } + return O.None[[]string]() + } + + res := FilterChain(f)(src) + + assert.Equal(t, From("a1", "b1", "a3", "b3"), res) +} + +func TestFilterMap(t *testing.T) { + src := From(1, 2, 3) + + f := func(i int) O.Option[string] { + if i%2 != 0 { + return O.Of(fmt.Sprintf("a%d", i)) + } + return O.None[string]() + } + + res := FilterMap(f)(src) + + assert.Equal(t, From("a1", "a3"), res) +} diff --git a/array/generic/array.go b/array/generic/array.go index b704169..6452440 100644 --- a/array/generic/array.go +++ b/array/generic/array.go @@ -118,6 +118,17 @@ func MonadFilterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(a A) O.Option[B]) return filterMap[GA, GB](fa, f) } +func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB { + return F.Flow2( + FilterMap[GA, []GB](f), + Flatten[[]GB], + ) +} + +func Flatten[GAA ~[]GA, GA ~[]A, A any](mma GAA) GA { + return MonadChain(mma, F.Identity[GA]) +} + func FilterMap[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[B]) func(GA) GB { return F.Bind2nd(MonadFilterMap[GA, GB, A, B], f) } diff --git a/endomorphism/monoid.go b/endomorphism/monoid.go index 50643a8..c4ef6dd 100644 --- a/endomorphism/monoid.go +++ b/endomorphism/monoid.go @@ -21,16 +21,12 @@ import ( S "github.com/IBM/fp-go/semigroup" ) -func concat[A any](first, second func(A) A) func(A) A { - return F.Flow2(first, second) -} - // Semigroup for the Endomorphism where the `concat` operation is the usual function composition. func Semigroup[A any]() S.Semigroup[func(A) A] { - return S.MakeSemigroup(concat[A]) + return S.MakeSemigroup(F.Flow2[func(A) A, func(A) A]) } // Monoid for the Endomorphism where the `concat` operation is the usual function composition. func Monoid[A any]() M.Monoid[func(A) A] { - return M.MakeMonoid(concat[A], F.Identity[A]) + return M.MakeMonoid(F.Flow2[func(A) A, func(A) A], F.Identity[A]) } diff --git a/iterator/stateless/generic/iterator.go b/iterator/stateless/generic/iterator.go index 40aa483..c3e6f9f 100644 --- a/iterator/stateless/generic/iterator.go +++ b/iterator/stateless/generic/iterator.go @@ -219,3 +219,10 @@ func Ap[GUV ~func() O.Option[T.Tuple2[GUV, func(U) V]], GV ~func() O.Option[T.Tu func MonadAp[GUV ~func() O.Option[T.Tuple2[GUV, func(U) V]], GV ~func() O.Option[T.Tuple2[GV, V]], GU ~func() O.Option[T.Tuple2[GU, U]], U, V any](fab GUV, ma GU) GV { return Ap[GUV, GV, GU](ma)(fab) } + +func FilterChain[GVV ~func() O.Option[T.Tuple2[GVV, GV]], GV ~func() O.Option[T.Tuple2[GV, V]], GU ~func() O.Option[T.Tuple2[GU, U]], FCT ~func(U) O.Option[GV], U, V any](f FCT) func(ma GU) GV { + return F.Flow2( + FilterMap[GVV, GU](f), + Flatten[GVV], + ) +} diff --git a/iterator/stateless/iterator.go b/iterator/stateless/iterator.go index 985aedf..76a8a4d 100644 --- a/iterator/stateless/iterator.go +++ b/iterator/stateless/iterator.go @@ -118,3 +118,8 @@ func Repeat[U any](n int, a U) Iterator[U] { func Count(start int) Iterator[int] { return G.Count[Iterator[int]](start) } + +// FilterChain filters and transforms the content of an iterator +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) +} diff --git a/record/doc.go b/record/doc.go new file mode 100644 index 0000000..76d6372 --- /dev/null +++ b/record/doc.go @@ -0,0 +1,17 @@ +// 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 record contains monadic operations for maps as well as a rich set of utility functions +package record diff --git a/record/generic/monoid.go b/record/generic/monoid.go index c555df9..ac36b8e 100644 --- a/record/generic/monoid.go +++ b/record/generic/monoid.go @@ -16,6 +16,7 @@ package generic import ( + F "github.com/IBM/fp-go/function" M "github.com/IBM/fp-go/monoid" S "github.com/IBM/fp-go/semigroup" ) @@ -26,3 +27,17 @@ func UnionMonoid[N ~map[K]V, K comparable, V any](s S.Semigroup[V]) M.Monoid[N] Empty[N](), ) } + +func UnionLastMonoid[N ~map[K]V, K comparable, V any]() M.Monoid[N] { + return M.MakeMonoid( + unionLast[N], + Empty[N](), + ) +} + +func UnionFirstMonoid[N ~map[K]V, K comparable, V any]() M.Monoid[N] { + return M.MakeMonoid( + F.Swap(unionLast[N]), + Empty[N](), + ) +} diff --git a/record/generic/record.go b/record/generic/record.go index 934cee2..cc1914c 100644 --- a/record/generic/record.go +++ b/record/generic/record.go @@ -19,6 +19,7 @@ import ( 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" T "github.com/IBM/fp-go/tuple" ) @@ -82,6 +83,34 @@ func MonadMap[M ~map[K]V, N ~map[K]R, K comparable, V, R any](r M, f func(V) R) return MonadMapWithIndex[M, N](r, F.Ignore1of2[K](f)) } +func MonadChainWithIndex[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](m Mo.Monoid[N], r M, f func(K, V1) N) N { + return G.ReduceWithIndex(r, func(k K, dst N, b V1) N { + return m.Concat(dst, f(k, b)) + }, m.Empty()) +} + +func MonadChain[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](m Mo.Monoid[N], r M, f func(V1) N) N { + return G.Reduce(r, func(dst N, b V1) N { + return m.Concat(dst, f(b)) + }, m.Empty()) +} + +func ChainWithIndex[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](m Mo.Monoid[N]) func(func(K, V1) N) func(M) N { + return func(f func(K, V1) N) func(M) N { + return func(ma M) N { + return MonadChainWithIndex(m, ma, f) + } + } +} + +func Chain[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](m Mo.Monoid[N]) func(func(V1) N) func(M) N { + return func(f func(V1) N) func(M) N { + return func(ma M) N { + return MonadChain(m, ma, f) + } + } +} + func MonadMapWithIndex[M ~map[K]V, N ~map[K]R, K comparable, V, R any](r M, f func(K, V) R) N { return G.ReduceWithIndex(r, func(k K, dst N, v V) N { return upsertAtReadWrite(dst, k, f(k, v)) @@ -114,15 +143,14 @@ func MapRefWithIndex[M ~map[K]V, N ~map[K]R, K comparable, V, R any](f func(K, * return F.Bind2nd(MonadMapRefWithIndex[M, N, K, V, R], f) } -func lookup[M ~map[K]V, K comparable, V any](r M, k K) O.Option[V] { - if val, ok := r[k]; ok { - return O.Some(val) - } - return O.None[V]() -} - func Lookup[M ~map[K]V, K comparable, V any](k K) func(M) O.Option[V] { - return F.Bind2nd(lookup[M, K, V], k) + n := O.None[V]() + return func(m M) O.Option[V] { + if val, ok := m[k]; ok { + return O.Some(val) + } + return n + } } func Has[M ~map[K]V, K comparable, V any](k K, r M) bool { @@ -161,6 +189,31 @@ func union[M ~map[K]V, K comparable, V any](m Mg.Magma[V], left M, right M) M { return result } +func unionLast[M ~map[K]V, K comparable, V any](left M, right M) M { + lenLeft := len(left) + + if lenLeft == 0 { + return right + } + + lenRight := len(right) + if lenRight == 0 { + return left + } + + result := make(M, lenLeft+lenRight) + + for k, v := range left { + result[k] = v + } + + for k, v := range right { + result[k] = v + } + + return result +} + func Union[M ~map[K]V, K comparable, V any](m Mg.Magma[V]) func(M) func(M) M { return func(right M) func(M) M { return func(left M) M { @@ -169,6 +222,22 @@ func Union[M ~map[K]V, K comparable, V any](m Mg.Magma[V]) func(M) func(M) M { } } +func UnionLast[M ~map[K]V, K comparable, V any](right M) func(M) M { + return func(left M) M { + return unionLast(left, right) + } +} + +func Merge[M ~map[K]V, K comparable, V any](right M) func(M) M { + return UnionLast(right) +} + +func UnionFirst[M ~map[K]V, K comparable, V any](right M) func(M) M { + return func(left M) M { + return unionLast(right, left) + } +} + func Empty[M ~map[K]V, K comparable, V any]() M { return make(M) } @@ -269,6 +338,33 @@ func FilterMap[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](f func(V1) O. return F.Bind2nd(filterMapWithIndex[M, N, K, V1, V2], F.Ignore1of2[K](f)) } +// Flatten converts a nested map into a regular map +func Flatten[M ~map[K]N, N ~map[K]V, K comparable, V any](m Mo.Monoid[N]) func(M) N { + return Chain[M, N](m)(F.Identity[N]) +} + +// FilterChainWithIndex creates a new map with only the elements for which the transformation function creates a Some +func FilterChainWithIndex[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](m Mo.Monoid[N]) func(func(K, V1) O.Option[N]) func(M) N { + flatten := Flatten[map[K]N, N](m) + return func(f func(K, V1) O.Option[N]) func(M) N { + return F.Flow2( + FilterMapWithIndex[M, map[K]N](f), + flatten, + ) + } +} + +// FilterChain creates a new map with only the elements for which the transformation function creates a Some +func FilterChain[M ~map[K]V1, N ~map[K]V2, K comparable, V1, V2 any](m Mo.Monoid[N]) func(func(V1) O.Option[N]) func(M) N { + flatten := Flatten[map[K]N, N](m) + return func(f func(V1) O.Option[N]) func(M) N { + return F.Flow2( + FilterMap[M, map[K]N](f), + flatten, + ) + } +} + // IsNil checks if the map is set to nil func IsNil[M ~map[K]V, K comparable, V any](m M) bool { return m == nil diff --git a/record/generic/semigroup.go b/record/generic/semigroup.go index 360aee9..8faaa8d 100644 --- a/record/generic/semigroup.go +++ b/record/generic/semigroup.go @@ -20,8 +20,19 @@ import ( ) func UnionSemigroup[N ~map[K]V, K comparable, V any](s S.Semigroup[V]) S.Semigroup[N] { - union := Union[N, K, V](s) return S.MakeSemigroup(func(first N, second N) N { - return union(second)(first) + return union(s, first, second) + }) +} + +func UnionLastSemigroup[N ~map[K]V, K comparable, V any]() S.Semigroup[N] { + return S.MakeSemigroup(func(first N, second N) N { + return unionLast(first, second) + }) +} + +func UnionFirstSemigroup[N ~map[K]V, K comparable, V any]() S.Semigroup[N] { + return S.MakeSemigroup(func(first N, second N) N { + return unionLast(second, first) }) } diff --git a/record/monoid.go b/record/monoid.go index 24a9469..c407208 100644 --- a/record/monoid.go +++ b/record/monoid.go @@ -21,6 +21,22 @@ import ( S "github.com/IBM/fp-go/semigroup" ) +// UnionMonoid computes the union of two maps of the same type func UnionMonoid[K comparable, V any](s S.Semigroup[V]) M.Monoid[map[K]V] { return G.UnionMonoid[map[K]V](s) } + +// UnionLastMonoid computes the union of two maps of the same type giving the last map precedence +func UnionLastMonoid[K comparable, V any]() M.Monoid[map[K]V] { + return G.UnionLastMonoid[map[K]V]() +} + +// UnionFirstMonoid computes the union of two maps of the same type giving the first map precedence +func UnionFirstMonoid[K comparable, V any]() M.Monoid[map[K]V] { + return G.UnionFirstMonoid[map[K]V]() +} + +// MergeMonoid computes the union of two maps of the same type giving the last map precedence +func MergeMonoid[K comparable, V any]() M.Monoid[map[K]V] { + return G.UnionLastMonoid[map[K]V]() +} diff --git a/record/record.go b/record/record.go index d92059e..7cc278f 100644 --- a/record/record.go +++ b/record/record.go @@ -17,27 +17,33 @@ package record import ( Mg "github.com/IBM/fp-go/magma" + Mo "github.com/IBM/fp-go/monoid" O "github.com/IBM/fp-go/option" G "github.com/IBM/fp-go/record/generic" T "github.com/IBM/fp-go/tuple" ) +// IsEmpty tests if a map is empty func IsEmpty[K comparable, V any](r map[K]V) bool { return G.IsEmpty(r) } +// IsNonEmpty tests if a map is not empty func IsNonEmpty[K comparable, V any](r map[K]V) bool { return G.IsNonEmpty(r) } +// Keys returns the key in a map func Keys[K comparable, V any](r map[K]V) []K { return G.Keys[map[K]V, []K](r) } +// Values returns the values in a map func Values[K comparable, V any](r map[K]V) []V { return G.Values[map[K]V, []V](r) } +// Collect applies a collector function to the key value pairs in a map and returns the result as an array func Collect[K comparable, V, R any](f func(K, V) R) func(map[K]V) []R { return G.Collect[map[K]V, []R](f) } @@ -90,10 +96,12 @@ func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) func(map[K]V) map[ return G.MapRefWithIndex[map[K]V, map[K]R](f) } +// Lookup returns the entry for a key in a map if it exists func Lookup[K comparable, V any](k K) func(map[K]V) O.Option[V] { return G.Lookup[map[K]V](k) } +// Has tests if a key is contained in a map func Has[K comparable, V any](k K, r map[K]V) bool { return G.Has(k, r) } @@ -102,10 +110,17 @@ func Union[K comparable, V any](m Mg.Magma[V]) func(map[K]V) func(map[K]V) map[K return G.Union[map[K]V](m) } +// Merge combines two maps giving the values in the right one precedence. Also refer to [MergeMonoid] +func Merge[K comparable, V any](right map[K]V) func(map[K]V) map[K]V { + return G.Merge[map[K]V](right) +} + +// Empty creates an empty map func Empty[K comparable, V any]() map[K]V { return G.Empty[map[K]V]() } +// Size returns the number of elements in a map func Size[K comparable, V any](r map[K]V) int { return G.Size(r) } @@ -130,6 +145,7 @@ func DeleteAt[K comparable, V any](k K) func(map[K]V) map[K]V { return G.DeleteAt[map[K]V](k) } +// Singleton creates a new map with a single entry func Singleton[K comparable, V any](k K, v V) map[K]V { return G.Singleton[map[K]V](k, v) } @@ -168,3 +184,34 @@ func IsNonNil[K comparable, V any](m map[K]V) bool { func ConstNil[K comparable, V any]() map[K]V { return (map[K]V)(nil) } + +func MonadChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(K, V1) map[K]V2) map[K]V2 { + return G.MonadChainWithIndex(m, r, f) +} + +func MonadChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(V1) map[K]V2) map[K]V2 { + return G.MonadChain(m, r, f) +} + +func ChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) map[K]V2) func(map[K]V1) map[K]V2 { + return G.ChainWithIndex[map[K]V1](m) +} + +func Chain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) map[K]V2) func(map[K]V1) map[K]V2 { + return G.Chain[map[K]V1](m) +} + +// Flatten converts a nested map into a regular map +func Flatten[K comparable, V any](m Mo.Monoid[map[K]V]) func(map[K]map[K]V) map[K]V { + return G.Flatten[map[K]map[K]V](m) +} + +// FilterChainWithIndex creates a new map with only the elements for which the transformation function creates a Some +func FilterChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 { + return G.FilterChainWithIndex[map[K]V1](m) +} + +// FilterChain creates a new map with only the elements for which the transformation function creates a Some +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) +} diff --git a/record/record_test.go b/record/record_test.go index e038b33..78610ca 100644 --- a/record/record_test.go +++ b/record/record_test.go @@ -16,6 +16,7 @@ package record import ( + "fmt" "sort" "testing" @@ -71,3 +72,30 @@ func TestLookup(t *testing.T) { assert.Equal(t, O.Some("a"), Lookup[string, string]("a")(data)) assert.Equal(t, O.None[string](), Lookup[string, string]("a1")(data)) } + +func TestFilterChain(t *testing.T) { + src := map[string]int{ + "a": 1, + "b": 2, + "c": 3, + } + + f := func(k string, value int) O.Option[map[string]string] { + if value%2 != 0 { + return O.Of(map[string]string{ + k: fmt.Sprintf("%s%d", k, value), + }) + } + return O.None[map[string]string]() + } + + // monoid + monoid := MergeMonoid[string, string]() + + res := FilterChainWithIndex[int](monoid)(f)(src) + + assert.Equal(t, map[string]string{ + "a": "a1", + "c": "c3", + }, res) +} diff --git a/record/semigroup.go b/record/semigroup.go index c72efc4..c08b385 100644 --- a/record/semigroup.go +++ b/record/semigroup.go @@ -22,3 +22,11 @@ import ( func UnionSemigroup[K comparable, V any](s S.Semigroup[V]) S.Semigroup[map[K]V] { return G.UnionSemigroup[map[K]V](s) } + +func UnionLastSemigroup[K comparable, V any]() S.Semigroup[map[K]V] { + return G.UnionLastSemigroup[map[K]V]() +} + +func UnionFirstSemigroup[K comparable, V any]() S.Semigroup[map[K]V] { + return G.UnionFirstSemigroup[map[K]V]() +}