diff --git a/README.md b/README.md index 32ac5c3..26699c4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,124 @@ -# fp-go -functional programming library for golang +# Functional programming library for golang + +![logo](resources/images/logo.png) + +## Design Goal + +This library aims to provide a set of data types and functions that make it easy and fun to write maintainable and testable code in golang. It encourages the following patterns: + +- write many small, testable and pure functions, i.e. functions that produce output only depending on their input and that do not execute side effects +- offer helpers to isolate side effects into lazily executed functions (IO) +- expose a consistent set of composition to create new functions from existing ones + - for each data type there exists a small set of composition functions + - these functions are called the same across all data types, so you only have to learn a small number of function names + - the semantic of functions of the same name is consistent across all data types + +### How does this play with the [🧘🏽 Zen Of Go](https://the-zen-of-go.netlify.app/)? + +#### 🧘🏽 Each package fulfils a single purpose + +βœ”οΈ Each of the top level packages (e.g. Option, Either, Task, ...) fulfils the purpose of defining the respective data type and implementing the set of common operations for this data type. + +#### 🧘🏽 Handle errors explicitly + +βœ”οΈ The library makes a clear distinction between that operations that cannot fail by design and operations that can fail. Failure is represented via the `Either` type and errors are handled explicitly by using `Either`'s monadic set of operations. + +#### 🧘🏽 Return early rather than nesting deeply + +βœ”οΈ We recommend to implement simple, small functions that implement one feature and that would typically not invoke other functions. Interaction with other functions is done by function composition and the composition makes sure to run one function after the other. In the error case the `Either` monad makes sure to skip the error path. + +#### 🧘🏽 Leave concurrency to the caller + +βœ”οΈ All operations are synchronous by default, including `Task`. Concurrency must be coded by the consumer of these functions explicitly, but the implementation is ready to deal with concurrent usage. + +#### 🧘🏽 Before you launch a goroutine, know when it will stop + +🀷🏽 This is left to the user of the library since the library itself will not start goroutines on its own. The Task monad offers support for cancellation via the golang context, though. + +#### 🧘🏽 Avoid package level state + +βœ”οΈ No package level state anywhere, this would be a significant anti-pattern + +#### 🧘🏽 Simplicity matters + +βœ”οΈ The library is simple in the sense that it offers a small, consistent interface to a variety of data types. Users can concentrate on implementing business logic rather than dealing with low level data structures. + +#### 🧘🏽 Write tests to lock in the behaviour of your package’s API + +🟑 The programming pattern suggested by this library encourages writing test cases. The library itself also has a growing number of tests, but not enough, yet. TBD + +#### 🧘🏽 If you think it’s slow, first prove it with a benchmark + +βœ”οΈ Absolutely. If you think the function composition offered by this library is too slow, please provide a benchmark. + +#### 🧘🏽 Moderation is a virtue + +βœ”οΈ The library does not implement its own goroutines and also does not require any expensive synchronization primitives. Coordination of Tasks is implemented via atomic counters without additional primitives. Channels are only used in the `Wait` function of a Task that should be invoked at most once in a complete application. + +#### 🧘🏽 Maintainability counts + +βœ”οΈ Code that consumes this library is easy to maintain because of the small and concise set of operations exposed. Also the suggested programming paradigm to decompose an application into small functions increases maintainability, because these functions are easy to understand and if they are pure, it's often sufficient to look at the type signature to understand the purpose. + +The library itself also comprises many small functions, but it's admittedly harder to maintain than code that uses it. However this asymmetry is intended because it offloads complexity from users into a central component. + +## Implementation Notes + +### Generics + +All monadic operations are implemented via generics, i.e. they offer a type safe way to compose operations. This allows for convenient IDE support and also gives confidence about the correctness of the composition at compile time. + +Downside is that this will result in different versions of each operation per type, these versions are generated by the golang compiler at build time (unlike type erasure in languages such as Java of TypeScript). This might lead to large binaries for codebases with many different types. If this is a concern, you can always implement type erasure on top, i.e. use the monadic operations with the `any` type as if generics were not supported. You loose type safety, but this might result in smaller binaries. + +### Use of the [~ Operator](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md) + +The FP library attempts to be easy to consume and one aspect of this is the definition of higher level type definitions instead of having to use their low level equivalent. It is e.g. more convenient and readable to use + +```go +TaskEither[E, A] +``` + +than + +```go +func(func(Either.Either[E, A])) +``` + +although both are logically equivalent. At the time of this writing the go type system does not support generic type aliases, only generic type definition, i.e. it is not possible to write: + +```go +type TaskEither[E, A any] = T.Task[ET.Either[E, A]] +``` + +only + +```go +type TaskEither[E, A any] T.Task[ET.Either[E, A]] +``` + +This makes a big difference, because in the second case the type `TaskEither[E, A any]` is considered a completely new type, not compatible to its right hand side, so it's not just a shortcut but a fully new type. + +From the implementation perspective however there is no reason to restrict the implementation to the new type, it can be generic for all compatible types. The way to express this in go is the [~](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md) operator. This comes with some quite complicated type declarations in some cases, which undermines the goal of the library to be easy to use. + +For that reason there exist sub-packages called `Generic` for all higher level types. These packages contain the fully generic implementation of the operations, preferring abstraction over usability. These packages are not meant to be used by end-users but are meant to be used by library extensions. The implementation for the convenient higher level types specializes the generic implementation for the particular higher level type, i.e. this layer does not contain any business logic but only *type magic*. + +### Higher Kinded Types + +Go does not support higher kinded types (HKT). Such types occur if a generic type itself is parametrized by another generic type. Example: + +The `Map` operation for `Task` is defined as: + +```go +func Map[A, B any](f func(A) B) func(Task[A]) Task[B] +``` + +and in fact the equivalent operations for all other mondas follow the same pattern, we could try to introduce a new type for `Task` (without a parameter) as a HKT, e.g. like so (made-up syntax, does not work in go): + +```go +func Map[HKT, A, B any](f func(A) B) func(HKT[A]) HKT[B] +``` + +this would be the completely generic method signature for all possible monads. In particular in many cases it is possible to compose functions independent of the concrete knowledge of the actual `HKT`. From the perspective of a library this is the ideal situation because then a particular algorithm only has to be implemented and tested once. + +This FP library addresses this by introducing the HKTs as individual types, e.g. `HKT[A]` would be represented as a new generic type `HKTA`. This loses the correlation to the type `A` but allows to implement generic algorithms, at the price of readability. + +For that reason these implementations are kept in the `internal` package. These are meant to be used by the library itself or by extensions, not by end users. diff --git a/apply/sequence.go b/apply/sequence.go new file mode 100644 index 0000000..5b23ca9 --- /dev/null +++ b/apply/sequence.go @@ -0,0 +1,72 @@ +package Apply + +import ( + F "github.com/ibm/fp-go/function" + T "github.com/ibm/fp-go/tuple" +) + +func tupleConstructor1[A any]() func(A) T.Tuple1[A] { + return F.Curry1(T.MakeTuple1[A]) +} + +func tupleConstructor2[A, B any]() func(A) func(B) T.Tuple2[A, B] { + return F.Curry2(T.MakeTuple2[A, B]) +} + +func tupleConstructor3[A, B, C any]() func(A) func(B) func(C) T.Tuple3[A, B, C] { + return F.Curry3(T.MakeTuple3[A, B, C]) +} + +func tupleConstructor4[A, B, C, D any]() func(A) func(B) func(C) func(D) T.Tuple4[A, B, C, D] { + return F.Curry4(T.MakeTuple4[A, B, C, D]) +} + +func SequenceT1[A, HKTA, HKT1A any]( + fmap func(HKTA, func(A) T.Tuple1[A]) HKT1A, + a HKTA) HKT1A { + return fmap(a, tupleConstructor1[A]()) +} + +// HKTA = HKT[A] +// HKTB = HKT[B] +// HKT2AB = HKT[Tuple[A, B]] +// HKTFB2AB = HKT[func(B)Tuple[A, B]] +func SequenceT2[A, B, HKTA, HKTB, HKTFB2AB, HKT2AB any]( + fmap func(HKTA, func(A) func(B) T.Tuple2[A, B]) HKTFB2AB, + fap1 func(HKTFB2AB, HKTB) HKT2AB, + a HKTA, b HKTB, +) HKT2AB { + return fap1(fmap(a, tupleConstructor2[A, B]()), b) +} + +// HKTA = HKT[A] +// HKTB = HKT[B] +// HKTC = HKT[C] +// HKT3ABC = HKT[Tuple[A, B, C]] +// HKTFB3ABC = HKT[func(B)func(C)Tuple[A, B, C]] +// HKTFC3ABC = HKT[func(C)Tuple[A, B, C]] +func SequenceT3[A, B, C, HKTA, HKTB, HKTC, HKTFB3ABC, HKTFC3ABC, HKT3ABC any]( + fmap func(HKTA, func(A) func(B) func(C) T.Tuple3[A, B, C]) HKTFB3ABC, + fap1 func(HKTFB3ABC, HKTB) HKTFC3ABC, + fap2 func(HKTFC3ABC, HKTC) HKT3ABC, + + a HKTA, b HKTB, c HKTC) HKT3ABC { + return fap2(fap1(fmap(a, tupleConstructor3[A, B, C]()), b), c) +} + +// HKTA = HKT[A] +// HKTB = HKT[B] +// HKTC = HKT[C] +// HKT3ABCD = HKT[Tuple[A, B, C, D]] +// HKTFB3ABCD = HKT[func(B)func(C)func(D)Tuple[A, B, C, D]] +// HKTFC3ABCD = HKT[func(C)func(D)Tuple[A, B, C, D]] +// HKTFD3ABCD = HKT[func(D)Tuple[A, B, C, D]] +func SequenceT4[A, B, C, D, HKTA, HKTB, HKTC, HKTD, HKTFB4ABCD, HKTFC4ABCD, HKTFD4ABCD, HKT4ABCD any]( + fmap func(HKTA, func(A) func(B) func(C) func(D) T.Tuple4[A, B, C, D]) HKTFB4ABCD, + fap1 func(HKTFB4ABCD, HKTB) HKTFC4ABCD, + fap2 func(HKTFC4ABCD, HKTC) HKTFD4ABCD, + fap3 func(HKTFD4ABCD, HKTD) HKT4ABCD, + + a HKTA, b HKTB, c HKTC, d HKTD) HKT4ABCD { + return fap3(fap2(fap1(fmap(a, tupleConstructor4[A, B, C, D]()), b), c), d) +} diff --git a/array/array.go b/array/array.go new file mode 100644 index 0000000..5351a04 --- /dev/null +++ b/array/array.go @@ -0,0 +1,279 @@ +package array + +import ( + G "github.com/ibm/fp-go/array/generic" + 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" +) + +// From constructs an array from a set of variadic arguments +func From[A any](data ...A) []A { + return G.From[[]A](data...) +} + +// MakeBy returns a `Array` of length `n` with element `i` initialized with `f(i)`. +func MakeBy[A any](n int, f func(int) A) []A { + // sanity check + if n <= 0 { + return Empty[A]() + } + // run the generator function across the input + as := make([]A, n) + for i := n - 1; i >= 0; i-- { + as[i] = f(i) + } + return as +} + +// Replicate creates a `Array` containing a value repeated the specified number of times. +func Replicate[A any](n int, a A) []A { + return MakeBy(n, F.Constant1[int](a)) +} + +func MonadMap[A, B any](as []A, f func(a A) B) []B { + return G.MonadMap[[]A, []B](as, f) +} + +func MonadMapRef[A, B any](as []A, f func(a *A) B) []B { + count := len(as) + bs := make([]B, count) + for i := count - 1; i >= 0; i-- { + bs[i] = f(&as[i]) + } + return bs +} + +func Map[A, B any](f func(a A) B) func([]A) []B { + return F.Bind2nd(MonadMap[A, B], f) +} + +func MapRef[A, B any](f func(a *A) B) func([]A) []B { + return F.Bind2nd(MonadMapRef[A, B], f) +} + +func filter[A any](fa []A, pred func(A) bool) []A { + var result []A + count := len(fa) + for i := 0; i < count; i++ { + a := fa[i] + if pred(a) { + result = append(result, a) + } + } + return result +} + +func filterRef[A any](fa []A, pred func(a *A) bool) []A { + var result []A + count := len(fa) + for i := 0; i < count; i++ { + a := fa[i] + if pred(&a) { + result = append(result, a) + } + } + return result +} + +func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B { + var result []B + count := len(fa) + for i := 0; i < count; i++ { + a := fa[i] + if pred(&a) { + result = append(result, f(&a)) + } + } + return result +} + +func Filter[A any](pred func(A) bool) func([]A) []A { + return F.Bind2nd(filter[A], pred) +} + +func FilterRef[A any](pred func(*A) bool) func([]A) []A { + return F.Bind2nd(filterRef[A], pred) +} + +func MonadFilterMap[A, B any](fa []A, f func(a A) O.Option[B]) []B { + return G.MonadFilterMap[[]A, []B](fa, f) +} + +func FilterMap[A, B any](f func(a A) O.Option[B]) func([]A) []B { + return G.FilterMap[[]A, []B](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) + } +} + +func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B { + current := initial + count := len(fa) + for i := 0; i < count; i++ { + current = f(current, &fa[i]) + } + return current +} + +func Reduce[A, B any](f func(B, A) B, initial B) func([]A) B { + return func(as []A) B { + return array.Reduce(as, f, initial) + } +} + +func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B { + return func(as []A) B { + return reduceRef(as, f, initial) + } +} + +func Append[A any](as []A, a A) []A { + return G.Append(as, a) +} + +func IsEmpty[A any](as []A) bool { + return array.IsEmpty(as) +} + +func IsNonEmpty[A any](as []A) bool { + return len(as) > 0 +} + +func Empty[A any]() []A { + return G.Empty[[]A]() +} + +func Zero[A any]() []A { + return Empty[A]() +} + +// Of constructs a single element array +func Of[A any](a A) []A { + return G.Of[[]A](a) +} + +func MonadChain[A, B any](fa []A, f func(a A) []B) []B { + return array.Reduce(fa, func(bs []B, a A) []B { + return append(bs, f(a)...) + }, Zero[B]()) +} + +func Chain[A, B any](f func(a A) []B) func([]A) []B { + return F.Bind2nd(MonadChain[A, B], f) +} + +func MonadAp[A, B any](fab []func(A) B, fa []A) []B { + return MonadChain(fab, F.Bind1st(MonadMap[A, B], fa)) +} + +func Ap[A, B any](fa []A) func([]func(A) B) []B { + return F.Bind2nd(MonadAp[A, B], fa) +} + +func Match[A, B any](onEmpty func() B, onNonEmpty func([]A) B) func([]A) B { + return func(as []A) B { + if IsEmpty(as) { + return onEmpty() + } + return onNonEmpty(as) + } +} + +func Tail[A any](as []A) O.Option[[]A] { + return G.Tail(as) +} + +func Head[A any](as []A) O.Option[A] { + return G.Head(as) +} + +func First[A any](as []A) O.Option[A] { + return G.First(as) +} + +func Last[A any](as []A) O.Option[A] { + return G.Last(as) +} + +func PrependAll[A any](middle A) func([]A) []A { + return func(as []A) []A { + count := len(as) + dst := count * 2 + result := make([]A, dst) + for i := count - 1; i >= 0; i-- { + dst-- + result[dst] = as[i] + dst-- + result[dst] = middle + } + return result + } +} + +func Intersperse[A any](middle A) func([]A) []A { + prepend := PrependAll(middle) + return func(as []A) []A { + if IsEmpty(as) { + return as + } + return prepend(as)[1:] + } +} + +func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A { + concatAll := ConcatAll[A](m)(m.Empty()) + return func(middle A) func([]A) A { + return Match(m.Empty, F.Flow2(Intersperse(middle), concatAll)) + } +} + +func Flatten[A any](mma [][]A) []A { + return MonadChain(mma, F.Identity[[]A]) +} + +func Slice[A any](low, high int) func(as []A) []A { + return array.Slice[[]A](low, high) +} + +func Lookup[A any](idx int) func([]A) O.Option[A] { + return G.Lookup[[]A](idx) +} + +func UpsertAt[A any](a A) func([]A) []A { + return G.UpsertAt[[]A](a) +} + +func Size[A any](as []A) int { + return G.Size(as) +} + +func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] { + return G.MonadPartition(as, pred) +} + +// Partition creates two new arrays out of one, the left result contains the elements +// for which the predicate returns false, the right one those for which the predicate returns true +func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] { + return G.Partition[[]A](pred) +} + +// IsNil checks if the array is set to nil +func IsNil[A any](as []A) bool { + return array.IsNil(as) +} + +// IsNonNil checks if the array is set to nil +func IsNonNil[A any](as []A) bool { + return array.IsNonNil(as) +} + +// ConstNil returns a nil array +func ConstNil[A any]() []A { + return array.ConstNil[[]A]() +} diff --git a/array/array_test.go b/array/array_test.go new file mode 100644 index 0000000..88fbe36 --- /dev/null +++ b/array/array_test.go @@ -0,0 +1,129 @@ +package array + +import ( + "strings" + "testing" + + 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" + T "github.com/ibm/fp-go/tuple" + "github.com/stretchr/testify/assert" +) + +func TestMap1(t *testing.T) { + + src := []string{"a", "b", "c"} + + up := Map(strings.ToUpper)(src) + + var up1 = []string{} + for _, s := range src { + up1 = append(up1, strings.ToUpper(s)) + } + + var up2 = []string{} + for i := range src { + up2 = append(up2, strings.ToUpper(src[i])) + } + + assert.Equal(t, up, up1) + assert.Equal(t, up, up2) +} + +func TestMap(t *testing.T) { + + mapper := Map(utils.Upper) + + src := []string{"a", "b", "c"} + + dst := mapper(src) + + assert.Equal(t, dst, []string{"A", "B", "C"}) +} + +func TestReduce(t *testing.T) { + + values := MakeBy(101, F.Identity[int]) + + sum := func(val int, current int) int { + return val + current + } + reducer := Reduce(sum, 0) + + result := reducer(values) + assert.Equal(t, result, 5050) + +} + +func TestEmpty(t *testing.T) { + assert.True(t, IsNonEmpty(MakeBy(101, F.Identity[int]))) + assert.True(t, IsEmpty([]int{})) +} + +func TestAp(t *testing.T) { + assert.Equal(t, + []int{2, 4, 6, 3, 6, 9}, + F.Pipe1( + []func(int) int{ + utils.Double, + utils.Triple, + }, + Ap[int, int]([]int{1, 2, 3}), + ), + ) +} + +func TestIntercalate(t *testing.T) { + is := Intercalate(S.Monoid)("-") + + assert.Equal(t, "", is(Empty[string]())) + assert.Equal(t, "a", is([]string{"a"})) + assert.Equal(t, "a-b-c", is([]string{"a", "b", "c"})) + assert.Equal(t, "a--c", is([]string{"a", "", "c"})) + assert.Equal(t, "a-b", is([]string{"a", "b"})) + assert.Equal(t, "a-b-c-d", is([]string{"a", "b", "c", "d"})) +} + +func TestPrependAll(t *testing.T) { + empty := Empty[int]() + prep := PrependAll(0) + assert.Equal(t, empty, prep(empty)) + assert.Equal(t, []int{0, 1, 0, 2, 0, 3}, prep([]int{1, 2, 3})) + assert.Equal(t, []int{0, 1}, prep([]int{1})) + assert.Equal(t, []int{0, 1, 0, 2, 0, 3, 0, 4}, prep([]int{1, 2, 3, 4})) +} + +func TestFlatten(t *testing.T) { + assert.Equal(t, []int{1, 2, 3}, Flatten([][]int{{1}, {2}, {3}})) +} + +func TestLookup(t *testing.T) { + data := []int{0, 1, 2} + none := O.None[int]() + + assert.Equal(t, none, Lookup[int](-1)(data)) + assert.Equal(t, none, Lookup[int](10)(data)) + assert.Equal(t, O.Some(1), Lookup[int](1)(data)) +} + +func TestSlice(t *testing.T) { + data := []int{0, 1, 2, 3} + + assert.Equal(t, []int{1, 2}, Slice[int](1, 3)(data)) +} + +func TestFrom(t *testing.T) { + assert.Equal(t, []int{1, 2, 3}, From(1, 2, 3)) +} + +func TestPartition(t *testing.T) { + + pred := func(n int) bool { + return n > 2 + } + + 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))) +} diff --git a/array/eq.go b/array/eq.go new file mode 100644 index 0000000..49cecbc --- /dev/null +++ b/array/eq.go @@ -0,0 +1,25 @@ +package array + +import ( + E "github.com/ibm/fp-go/eq" +) + +func equals[T any](left []T, right []T, eq func(T, T) bool) bool { + if len(left) != len(right) { + return false + } + for i, v1 := range left { + v2 := right[i] + if !eq(v1, v2) { + return false + } + } + return true +} + +func Eq[T any](e E.Eq[T]) E.Eq[[]T] { + eq := e.Equals + return E.FromEquals(func(left, right []T) bool { + return equals(left, right, eq) + }) +} diff --git a/array/generic/array.go b/array/generic/array.go new file mode 100644 index 0000000..f652fa7 --- /dev/null +++ b/array/generic/array.go @@ -0,0 +1,109 @@ +package generic + +import ( + F "github.com/ibm/fp-go/function" + "github.com/ibm/fp-go/internal/array" + O "github.com/ibm/fp-go/option" + "github.com/ibm/fp-go/tuple" +) + +// Of constructs a single element array +func Of[GA ~[]A, A any](value A) GA { + return GA{value} +} + +// From constructs an array from a set of variadic arguments +func From[GA ~[]A, A any](data ...A) GA { + return data +} + +func Lookup[GA ~[]A, A any](idx int) func(GA) O.Option[A] { + none := O.None[A]() + if idx < 0 { + return F.Constant1[GA](none) + } + return func(as GA) O.Option[A] { + if idx < len(as) { + return O.Some(as[idx]) + } + return none + } +} + +func Tail[GA ~[]A, A any](as GA) O.Option[GA] { + if array.IsEmpty(as) { + return O.None[GA]() + } + return O.Some(as[1:]) +} + +func Head[GA ~[]A, A any](as GA) O.Option[A] { + if array.IsEmpty(as) { + return O.None[A]() + } + return O.Some(as[0]) +} + +func First[GA ~[]A, A any](as GA) O.Option[A] { + return Head(as) +} + +func Last[GA ~[]A, A any](as GA) O.Option[A] { + if array.IsEmpty(as) { + return O.None[A]() + } + return O.Some(as[len(as)-1]) +} + +func Append[GA ~[]A, A any](as GA, a A) GA { + return array.Append(as, a) +} + +func Empty[GA ~[]A, A any]() GA { + return array.Empty[GA]() +} + +func UpsertAt[GA ~[]A, A any](a A) func(GA) GA { + return array.UpsertAt[GA](a) +} + +func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB { + return array.MonadMap[GA, GB](as, f) +} + +func Size[GA ~[]A, A any](as GA) int { + return len(as) +} + +func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(a A) O.Option[B]) GB { + return array.Reduce(fa, func(bs GB, a A) GB { + return O.MonadFold(f(a), F.Constant(bs), F.Bind1st(Append[GB, B], bs)) + }, Empty[GB]()) +} + +func MonadFilterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(a A) O.Option[B]) GB { + return filterMap[GA, GB](fa, f) +} + +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) +} + +func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] { + left := Empty[GA]() + right := Empty[GA]() + array.Reduce(as, func(c bool, a A) bool { + if pred(a) { + right = append(right, a) + } else { + left = append(left, a) + } + return c + }, true) + // returns the partition + return tuple.MakeTuple2(left, right) +} + +func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] { + return F.Bind2nd(MonadPartition[GA, A], pred) +} diff --git a/array/generic/sort.go b/array/generic/sort.go new file mode 100644 index 0000000..4e63cb3 --- /dev/null +++ b/array/generic/sort.go @@ -0,0 +1,26 @@ +package generic + +import ( + "sort" + + 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 func(ma GA) GA { + // nothing to sort + l := len(ma) + if l < 2 { + return ma + } + // copy + cpy := make(GA, l) + copy(cpy, ma) + sort.Slice(cpy, func(i, j int) bool { + return ord.Compare(cpy[i], cpy[j]) < 0 + }) + return cpy + } +} diff --git a/array/magma.go b/array/magma.go new file mode 100644 index 0000000..ae82396 --- /dev/null +++ b/array/magma.go @@ -0,0 +1,10 @@ +package array + +import ( + F "github.com/ibm/fp-go/function" + M "github.com/ibm/fp-go/magma" +) + +func ConcatAll[A any](m M.Magma[A]) func(A) func([]A) A { + return F.Bind1st(Reduce[A, A], m.Concat) +} diff --git a/array/magma_test.go b/array/magma_test.go new file mode 100644 index 0000000..077a186 --- /dev/null +++ b/array/magma_test.go @@ -0,0 +1,21 @@ +package array + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + M "github.com/ibm/fp-go/magma" +) + +var subInt = M.MakeMagma(func(first int, second int) int { + return first - second +}) + +func TestConcatAll(t *testing.T) { + + var subAll = ConcatAll(subInt)(0) + + assert.Equal(t, subAll([]int{1, 2, 3}), -6) + +} diff --git a/array/monoid.go b/array/monoid.go new file mode 100644 index 0000000..c869e60 --- /dev/null +++ b/array/monoid.go @@ -0,0 +1,43 @@ +package array + +import ( + "github.com/ibm/fp-go/internal/array" + M "github.com/ibm/fp-go/monoid" +) + +func concat[T any](left, right []T) []T { + // some performance checks + ll := len(left) + lr := len(right) + if ll == 0 { + return right + } + if lr == 0 { + return left + } + // need to copy + buf := make([]T, ll+lr) + copy(buf[copy(buf, left):], right) + return buf +} + +func Monoid[T any]() M.Monoid[[]T] { + return M.MakeMonoid(concat[T], Empty[T]()) +} + +func addLen[A any](count int, data []A) int { + return count + len(data) +} + +// ConcatAll efficiently concatenates the input arrays into a final array +func ArrayConcatAll[A any](data ...[]A) []A { + // get the full size + count := array.Reduce(data, addLen[A], 0) + buf := make([]A, count) + // copy + array.Reduce(data, func(idx int, seg []A) int { + return idx + copy(buf[idx:], seg) + }, 0) + // returns the final array + return buf +} diff --git a/array/monoid_test.go b/array/monoid_test.go new file mode 100644 index 0000000..5c7cc05 --- /dev/null +++ b/array/monoid_test.go @@ -0,0 +1,11 @@ +package array + +import ( + "testing" + + M "github.com/ibm/fp-go/monoid/testing" +) + +func TestMonoid(t *testing.T) { + M.AssertLaws(t, Monoid[int]())([][]int{{}, {1}, {1, 2}}) +} diff --git a/array/sequence.go b/array/sequence.go new file mode 100644 index 0000000..513ca69 --- /dev/null +++ b/array/sequence.go @@ -0,0 +1,43 @@ +package array + +import ( + F "github.com/ibm/fp-go/function" + O "github.com/ibm/fp-go/option" +) + +// We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces + +// HKTA = HKT +// HKTRA = HKT<[]A> +// HKTFRA = HKT + +// Sequence takes an `Array` where elements are `HKT` (higher kinded type) and, +// using an applicative of that `HKT`, returns an `HKT` of `[]A`. +// e.g. it can turn an `[]Either[error, string]` into an `Either[error, []string]`. +// +// Sequence requires an `Applicative` of the `HKT` you are targeting, e.g. to turn an +// `[]Either[E, A]` into an `Either[E, []A]`, it needs an +// Applicative` for `Either`, to to turn an `[]Option[A]` into an `Option[ []A]`, +// it needs an `Applicative` for `Option`. +func Sequence[A, HKTA, HKTRA, HKTFRA any]( + _of func([]A) HKTRA, + _map func(HKTRA, func([]A) func(A) []A) HKTFRA, + _ap func(HKTFRA, HKTA) HKTRA, +) func([]HKTA) HKTRA { + ca := F.Curry2(Append[A]) + return Reduce(func(fas HKTRA, fa HKTA) HKTRA { + return _ap( + _map(fas, ca), + fa, + ) + }, _of(Empty[A]())) +} + +// ArrayOption returns a function to convert sequence of options into an option of a sequence +func ArrayOption[A any]() func([]O.Option[A]) O.Option[[]A] { + return Sequence( + O.Of[[]A], + O.MonadMap[[]A, func(A) []A], + O.MonadAp[A, []A], + ) +} diff --git a/array/sequence_test.go b/array/sequence_test.go new file mode 100644 index 0000000..e05a1b6 --- /dev/null +++ b/array/sequence_test.go @@ -0,0 +1,16 @@ +package array + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + O "github.com/ibm/fp-go/option" +) + +func TestSequenceOption(t *testing.T) { + seq := ArrayOption[int]() + + assert.Equal(t, O.Of([]int{1, 3}), seq([]O.Option[int]{O.Of(1), O.Of(3)})) + assert.Equal(t, O.None[[]int](), seq([]O.Option[int]{O.Of(1), O.None[int]()})) +} diff --git a/array/sort.go b/array/sort.go new file mode 100644 index 0000000..4b94ffd --- /dev/null +++ b/array/sort.go @@ -0,0 +1,11 @@ +package array + +import ( + G "github.com/ibm/fp-go/array/generic" + O "github.com/ibm/fp-go/ord" +) + +// Sort implements a stable sort on the array given the provided ordering +func Sort[T any](ord O.Ord[T]) func(ma []T) []T { + return G.Sort[[]T](ord) +} diff --git a/array/sort_test.go b/array/sort_test.go new file mode 100644 index 0000000..124132a --- /dev/null +++ b/array/sort_test.go @@ -0,0 +1,21 @@ +package array + +import ( + "testing" + + O "github.com/ibm/fp-go/ord" + "github.com/stretchr/testify/assert" +) + +func TestSort(t *testing.T) { + + ordInt := O.FromStrictCompare[int]() + + input := []int{2, 1, 3} + + res := Sort(ordInt)(input) + + assert.Equal(t, []int{1, 2, 3}, res) + assert.Equal(t, []int{2, 1, 3}, input) + +} diff --git a/array/traverse.go b/array/traverse.go new file mode 100644 index 0000000..1fa62ce --- /dev/null +++ b/array/traverse.go @@ -0,0 +1,23 @@ +package array + +import "github.com/ibm/fp-go/internal/array" + +func Traverse[A, B, HKTB, HKTAB, HKTRB any]( + _of func([]B) HKTRB, + _map func(HKTRB, func([]B) func(B) []B) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + f func(A) HKTB) func([]A) HKTRB { + return array.Traverse[[]A](_of, _map, _ap, f) +} + +func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any]( + _of func([]B) HKTRB, + _map func(HKTRB, func([]B) func(B) []B) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + ta []A, + f func(A) HKTB) HKTRB { + + return array.MonadTraverse(_of, _map, _ap, ta, f) +} diff --git a/array/traverse_test.go b/array/traverse_test.go new file mode 100644 index 0000000..f50449e --- /dev/null +++ b/array/traverse_test.go @@ -0,0 +1,28 @@ +package array + +import ( + "testing" + + O "github.com/ibm/fp-go/option" + "github.com/stretchr/testify/assert" +) + +type ArrayType = []int + +func TestTraverse(t *testing.T) { + + traverse := Traverse( + O.Of[ArrayType], + O.MonadMap[ArrayType, func(int) ArrayType], + O.MonadAp[int, ArrayType], + + func(n int) O.Option[int] { + if n%2 == 0 { + return O.None[int]() + } + return O.Of(n) + }) + + assert.Equal(t, O.None[[]int](), traverse(ArrayType{1, 2})) + assert.Equal(t, O.Of(ArrayType{1, 3}), traverse(ArrayType{1, 3})) +} diff --git a/constraints/constraints.go b/constraints/constraints.go new file mode 100644 index 0000000..ebab42d --- /dev/null +++ b/constraints/constraints.go @@ -0,0 +1,21 @@ +package constraints + +type Ordered interface { + Integer | Float | ~string +} + +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +type Integer interface { + Signed | Unsigned +} + +type Float interface { + ~float32 | ~float64 +} diff --git a/either/apply.go b/either/apply.go new file mode 100644 index 0000000..808ea3e --- /dev/null +++ b/either/apply.go @@ -0,0 +1,14 @@ +package either + +import ( + M "github.com/ibm/fp-go/monoid" + S "github.com/ibm/fp-go/semigroup" +) + +func ApplySemigroup[E, A any](s S.Semigroup[A]) S.Semigroup[Either[E, A]] { + return S.ApplySemigroup(MonadMap[E, A, func(A) A], MonadAp[E, A, A], s) +} + +func ApplicativeMonoid[E, A any](m M.Monoid[A]) M.Monoid[Either[E, A]] { + return M.ApplicativeMonoid(Of[E, A], MonadMap[E, A, func(A) A], MonadAp[E, A, A], m) +} diff --git a/either/apply_test.go b/either/apply_test.go new file mode 100644 index 0000000..d7c66e7 --- /dev/null +++ b/either/apply_test.go @@ -0,0 +1,48 @@ +package either + +import ( + "testing" + + M "github.com/ibm/fp-go/monoid/testing" + N "github.com/ibm/fp-go/number" + "github.com/stretchr/testify/assert" +) + +func TestApplySemigroup(t *testing.T) { + + sg := ApplySemigroup[string](N.SemigroupSum[int]()) + + la := Left[string, int]("a") + lb := Left[string, int]("b") + r1 := Right[string](1) + r2 := Right[string](2) + r3 := Right[string](3) + + assert.Equal(t, la, sg.Concat(la, lb)) + assert.Equal(t, lb, sg.Concat(r1, lb)) + assert.Equal(t, la, sg.Concat(la, r2)) + assert.Equal(t, lb, sg.Concat(r1, lb)) + assert.Equal(t, r3, sg.Concat(r1, r2)) +} + +func TestApplicativeMonoid(t *testing.T) { + + m := ApplicativeMonoid[string](N.MonoidSum[int]()) + + la := Left[string, int]("a") + lb := Left[string, int]("b") + r1 := Right[string](1) + r2 := Right[string](2) + r3 := Right[string](3) + + assert.Equal(t, la, m.Concat(la, m.Empty())) + assert.Equal(t, lb, m.Concat(m.Empty(), lb)) + assert.Equal(t, r1, m.Concat(r1, m.Empty())) + assert.Equal(t, r2, m.Concat(m.Empty(), r2)) + assert.Equal(t, r3, m.Concat(r1, r2)) +} + +func TestApplicativeMonoidLaws(t *testing.T) { + m := ApplicativeMonoid[string](N.MonoidSum[int]()) + M.AssertLaws(t, m)([]Either[string, int]{Left[string, int]("a"), Right[string](1)}) +} diff --git a/either/curry.go b/either/curry.go new file mode 100644 index 0000000..a08c0a2 --- /dev/null +++ b/either/curry.go @@ -0,0 +1,61 @@ +package either + +import ( + F "github.com/ibm/fp-go/function" +) + +// these function curry a golang function that returns an error into its curried version that returns an either + +func Curry0[R any](f func() (R, error)) func() Either[error, R] { + return Eitherize0(f) +} + +func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] { + return Eitherize1(f) +} + +func Curry2[T1, T2, R any](f func(T1, T2) (R, error)) func(T1) func(T2) Either[error, R] { + return F.Curry2(Eitherize2(f)) +} + +func Curry3[T1, T2, T3, R any](f func(T1, T2, T3) (R, error)) func(T1) func(T2) func(T3) Either[error, R] { + return F.Curry3(Eitherize3(f)) +} + +func Curry4[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) (R, error)) func(T1) func(T2) func(T3) func(T4) Either[error, R] { + return F.Curry4(Eitherize4(f)) +} + +func Uncurry0[R any](f func() Either[error, R]) func() (R, error) { + return func() (R, error) { + return UnwrapError(f()) + } +} + +func Uncurry1[T1, R any](f func(T1) Either[error, R]) func(T1) (R, error) { + uc := F.Uncurry1(f) + return func(t1 T1) (R, error) { + return UnwrapError(uc(t1)) + } +} + +func Uncurry2[T1, T2, R any](f func(T1) func(T2) Either[error, R]) func(T1, T2) (R, error) { + uc := F.Uncurry2(f) + return func(t1 T1, t2 T2) (R, error) { + return UnwrapError(uc(t1, t2)) + } +} + +func Uncurry3[T1, T2, T3, R any](f func(T1) func(T2) func(T3) Either[error, R]) func(T1, T2, T3) (R, error) { + uc := F.Uncurry3(f) + return func(t1 T1, t2 T2, t3 T3) (R, error) { + return UnwrapError(uc(t1, t2, t3)) + } +} + +func Uncurry4[T1, T2, T3, T4, R any](f func(T1) func(T2) func(T3) func(T4) Either[error, R]) func(T1, T2, T3, T4) (R, error) { + uc := F.Uncurry4(f) + return func(t1 T1, t2 T2, t3 T3, t4 T4) (R, error) { + return UnwrapError(uc(t1, t2, t3, t4)) + } +} diff --git a/either/either.go b/either/either.go new file mode 100644 index 0000000..ad1b61b --- /dev/null +++ b/either/either.go @@ -0,0 +1,344 @@ +// package either implements the Either monad +// +// A data type that can be of either of two types but not both. This is +// typically used to carry an error or a return value +package either + +import ( + E "github.com/ibm/fp-go/errors" + F "github.com/ibm/fp-go/function" + O "github.com/ibm/fp-go/option" +) + +func Of[E, A any](value A) Either[E, A] { + return F.Pipe1(value, Right[E, A]) +} + +func FromIO[E, A any](f func() A) Either[E, A] { + return F.Pipe1(f(), Right[E, A]) +} + +func MonadAp[E, A, B any](fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] { + return fold(fab, Left[E, B], func(ab func(A) B) Either[E, B] { + return fold(fa, Left[E, B], F.Flow2(ab, Right[E, B])) + }) +} + +func Ap[E, A, B any](fa Either[E, A]) func(fab Either[E, func(a A) B]) Either[E, B] { + return F.Bind2nd(MonadAp[E, A, B], fa) +} + +func MonadMap[E, A, B any](fa Either[E, A], f func(a A) B) Either[E, B] { + return MonadChain(fa, F.Flow2(f, Right[E, B])) +} + +func MonadBiMap[E1, E2, A, B any](fa Either[E1, A], f func(E1) E2, g func(a A) B) Either[E2, B] { + return fold(fa, F.Flow2(f, Left[E2, B]), F.Flow2(g, Right[E2, B])) +} + +// BiMap maps a pair of functions over the two type arguments of the bifunctor. +func BiMap[E1, E2, A, B any](f func(E1) E2, g func(a A) B) func(Either[E1, A]) Either[E2, B] { + return Fold(F.Flow2(f, Left[E2, B]), F.Flow2(g, Right[E2, B])) +} + +func MonadMapTo[E, A, B any](fa Either[E, A], b B) Either[E, B] { + return MonadMap(fa, F.Constant1[A](b)) +} + +func MapTo[E, A, B any](b B) func(Either[E, A]) Either[E, B] { + return F.Bind2nd(MonadMapTo[E, A, B], b) +} + +func MonadMapLeft[E, A, B any](fa Either[E, A], f func(E) B) Either[B, A] { + return fold(fa, F.Flow2(f, Left[B, A]), Right[B, A]) +} + +func Map[E, A, B any](f func(a A) B) func(fa Either[E, A]) Either[E, B] { + return Chain(F.Flow2(f, Right[E, B])) +} + +func MapLeft[E, A, B any](f func(E) B) func(fa Either[E, A]) Either[B, A] { + return F.Bind2nd(MonadMapLeft[E, A, B], f) +} + +func MonadChain[E, A, B any](fa Either[E, A], f func(a A) Either[E, B]) Either[E, B] { + return fold(fa, Left[E, B], f) +} + +func MonadChainFirst[E, A, B any](ma Either[E, A], f func(a A) Either[E, B]) Either[E, A] { + return MonadChain(ma, func(a A) Either[E, A] { + return MonadMap(f(a), F.Constant1[B](a)) + }) +} + +func MonadChainTo[E, A, B any](ma Either[E, A], mb Either[E, B]) Either[E, B] { + return mb +} + +func MonadChainOptionK[E, A, B any](onNone func() E, ma Either[E, A], f func(A) O.Option[B]) Either[E, B] { + return MonadChain(ma, F.Flow2(f, FromOption[E, B](onNone))) +} + +func ChainOptionK[E, A, B any](onNone func() E) func(func(A) O.Option[B]) func(Either[E, A]) Either[E, B] { + from := FromOption[E, B](onNone) + return func(f func(A) O.Option[B]) func(Either[E, A]) Either[E, B] { + return Chain(F.Flow2(f, from)) + } +} + +func ChainTo[E, A, B any](mb Either[E, B]) func(Either[E, A]) Either[E, B] { + return F.Bind2nd(MonadChainTo[E, A, B], mb) +} + +func Chain[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, B] { + return F.Bind2nd(MonadChain[E, A, B], f) +} + +func ChainFirst[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, A] { + return F.Bind2nd(MonadChainFirst[E, A, B], f) +} + +func Flatten[E, A any](mma Either[E, Either[E, A]]) Either[E, A] { + return MonadChain(mma, F.Identity[Either[E, A]]) +} + +func TryCatch[E, A any](f func() (A, error), onThrow func(error) E) Either[E, A] { + val, err := f() + if err != nil { + return F.Pipe2(err, onThrow, Left[E, A]) + } + return F.Pipe1(val, Right[E, A]) +} + +func TryCatchErrorG[GA ~func() (A, error), A any](f GA) Either[error, A] { + return TryCatch(f, E.IdentityError) +} + +func TryCatchError[A any](f func() (A, error)) Either[error, A] { + return TryCatchErrorG(f) +} + +func Sequence2[E, T1, T2, R any](f func(T1, T2) Either[E, R]) func(Either[E, T1], Either[E, T2]) Either[E, R] { + return func(e1 Either[E, T1], e2 Either[E, T2]) Either[E, R] { + return MonadSequence2(e1, e2, f) + } +} + +func Sequence3[E, T1, T2, T3, R any](f func(T1, T2, T3) Either[E, R]) func(Either[E, T1], Either[E, T2], Either[E, T3]) Either[E, R] { + return func(e1 Either[E, T1], e2 Either[E, T2], e3 Either[E, T3]) Either[E, R] { + return MonadSequence3(e1, e2, e3, f) + } +} + +func FromOption[E, A any](onNone func() E) func(O.Option[A]) Either[E, A] { + return O.Fold(F.Nullary2(onNone, Left[E, A]), Right[E, A]) +} + +func ToOption[E, A any]() func(Either[E, A]) O.Option[A] { + return Fold(F.Ignore1[E](O.None[A]), O.Some[A]) +} + +func FromError[A any](f func(a A) error) func(A) Either[error, A] { + return func(a A) Either[error, A] { + return TryCatchError(func() (A, error) { + return a, f(a) + }) + } +} + +func ToError[A any](e Either[error, A]) error { + return fold(e, E.IdentityError, F.Constant1[A, error](nil)) +} + +func Eitherize0G[GA ~func() (R, error), GB ~func() Either[error, R], R any](f GA) GB { + return F.Bind1(TryCatchErrorG[GA, R], f) +} + +func Eitherize0[R any](f func() (R, error)) func() Either[error, R] { + return Eitherize0G[func() (R, error), func() Either[error, R]](f) +} + +func Uneitherize0G[GA ~func() Either[error, R], GB ~func() (R, error), R any](f GA) GB { + return func() (R, error) { + return UnwrapError(f()) + } +} + +func Uneitherize0[R any](f func() Either[error, R]) func() (R, error) { + return Uneitherize0G[func() Either[error, R], func() (R, error)](f) +} + +func Eitherize1G[GA ~func(T1) (R, error), GB ~func(T1) Either[error, R], T1, R any](f GA) GB { + return func(t1 T1) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1) + }) + } +} + +func Eitherize1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] { + return Eitherize1G[func(T1) (R, error), func(T1) Either[error, R]](f) +} + +func Uneitherize1G[GA ~func(T1) Either[error, R], GB ~func(T1) (R, error), T1, R any](f GA) GB { + return func(t1 T1) (R, error) { + return UnwrapError(f(t1)) + } +} + +func Uneitherize1[T1, R any](f func(T1) Either[error, R]) func(T1) (R, error) { + return Uneitherize1G[func(T1) Either[error, R], func(T1) (R, error)](f) +} + +func Eitherize2G[GA ~func(t1 T1, t2 T2) (R, error), GB ~func(t1 T1, t2 T2) Either[error, R], T1, T2, R any](f GA) GB { + return func(t1 T1, t2 T2) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2) + }) + } +} + +func Eitherize2[T1, T2, R any](f func(t1 T1, t2 T2) (R, error)) func(t1 T1, t2 T2) Either[error, R] { + return Eitherize2G[func(t1 T1, t2 T2) (R, error), func(t1 T1, t2 T2) Either[error, R]](f) +} + +func Uneitherize2G[GA ~func(T1, T2) Either[error, R], GB ~func(T1, T2) (R, error), T1, T2, R any](f GA) GB { + return func(t1 T1, t2 T2) (R, error) { + return UnwrapError(f(t1, t2)) + } +} + +func Uneitherize2[T1, T2, R any](f func(T1, T2) Either[error, R]) func(T1, T2) (R, error) { + return Uneitherize2G[func(T1, T2) Either[error, R], func(T1, T2) (R, error)](f) +} + +func Eitherize3G[GA ~func(t1 T1, t2 T2, t3 T3) (R, error), GB ~func(t1 T1, t2 T2, t3 T3) Either[error, R], T1, T2, T3, R any](f GA) GB { + return func(t1 T1, t2 T2, t3 T3) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, t3) + }) + } +} + +func Eitherize3[T1, T2, T3, R any](f func(t1 T1, t2 T2, t3 T3) (R, error)) func(t1 T1, t2 T2, t3 T3) Either[error, R] { + return Eitherize3G[func(t1 T1, t2 T2, t3 T3) (R, error), func(t1 T1, t2 T2, t3 T3) Either[error, R]](f) +} + +func Uneitherize3G[GA ~func(T1, T2, T3) Either[error, R], GB ~func(T1, T2, T3) (R, error), T1, T2, T3, R any](f GA) GB { + return func(t1 T1, t2 T2, t3 T3) (R, error) { + return UnwrapError(f(t1, t2, t3)) + } +} + +func Uneitherize3[T1, T2, T3, R any](f func(T1, T2, T3) Either[error, R]) func(T1, T2, T3) (R, error) { + return Uneitherize3G[func(T1, T2, T3) Either[error, R], func(T1, T2, T3) (R, error)](f) +} + +func Eitherize4G[GA ~func(t1 T1, t2 T2, t3 T3, t4 T4) (R, error), GB ~func(t1 T1, t2 T2, t3 T3, t4 T4) Either[error, R], T1, T2, T3, T4, R any](f GA) GB { + return func(t1 T1, t2 T2, t3 T3, t4 T4) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, t3, t4) + }) + } +} + +func Eitherize4[T1, T2, T3, T4, R any](f func(t1 T1, t2 T2, t3 T3, t4 T4) (R, error)) func(t1 T1, t2 T2, t3 T3, t4 T4) Either[error, R] { + return Eitherize4G[func(t1 T1, t2 T2, t3 T3, t4 T4) (R, error), func(t1 T1, t2 T2, t3 T3, t4 T4) Either[error, R]](f) +} + +func Uneitherize4G[GA ~func(T1, T2, T3, T4) Either[error, R], GB ~func(T1, T2, T3, T4) (R, error), T1, T2, T3, T4, R any](f GA) GB { + return func(t1 T1, t2 T2, t3 T3, t4 T4) (R, error) { + return UnwrapError(f(t1, t2, t3, t4)) + } +} + +func Uneitherize4[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) Either[error, R]) func(T1, T2, T3, T4) (R, error) { + return Uneitherize4G[func(T1, T2, T3, T4) Either[error, R], func(T1, T2, T3, T4) (R, error)](f) +} + +func MonadFold[E, A, B any](ma Either[E, A], onLeft func(e E) B, onRight func(a A) B) B { + return fold(ma, onLeft, onRight) +} + +func Fold[E, A, B any](onLeft func(E) B, onRight func(A) B) func(Either[E, A]) B { + return func(ma Either[E, A]) B { + return fold(ma, onLeft, onRight) + } +} + +func UnwrapError[A any](ma Either[error, A]) (A, error) { + return Unwrap[error](ma) +} + +func FromPredicate[E, A any](pred func(A) bool, onFalse func(A) E) func(A) Either[E, A] { + return func(a A) Either[E, A] { + if pred(a) { + return Right[E](a) + } + return Left[E, A](onFalse(a)) + } +} + +func FromNillable[E, A any](e E) func(*A) Either[E, *A] { + return FromPredicate(F.IsNonNil[A], F.Constant1[*A](e)) +} + +func GetOrElse[E, A any](onLeft func(E) A) func(Either[E, A]) A { + return Fold(onLeft, F.Identity[A]) +} + +func Reduce[E, A, B any](f func(B, A) B, initial B) func(Either[E, A]) B { + return Fold( + F.Constant1[E](initial), + F.Bind1st(f, initial), + ) +} + +func AltW[E, E1, A any](that func() Either[E1, A]) func(Either[E, A]) Either[E1, A] { + return Fold(F.Ignore1[E](that), Right[E1, A]) +} + +func Alt[E, A any](that func() Either[E, A]) func(Either[E, A]) Either[E, A] { + return AltW[E](that) +} + +func OrElse[E, A any](onLeft func(e E) Either[E, A]) func(Either[E, A]) Either[E, A] { + return Fold(onLeft, Of[E, A]) +} + +func ToType[E, A any](onError func(any) E) func(any) Either[E, A] { + return func(value any) Either[E, A] { + return F.Pipe2( + value, + O.ToType[A], + O.Fold(F.Nullary3(F.Constant(value), onError, Left[E, A]), Right[E, A]), + ) + } +} + +func Memoize[E, A any](val Either[E, A]) Either[E, A] { + return val +} + +func MonadSequence2[E, T1, T2, R any](e1 Either[E, T1], e2 Either[E, T2], f func(T1, T2) Either[E, R]) Either[E, R] { + return fold(e1, Left[E, R], func(t1 T1) Either[E, R] { + return fold(e2, Left[E, R], func(t2 T2) Either[E, R] { + return f(t1, t2) + }) + }) +} + +func MonadSequence3[E, T1, T2, T3, R any](e1 Either[E, T1], e2 Either[E, T2], e3 Either[E, T3], f func(T1, T2, T3) Either[E, R]) Either[E, R] { + return fold(e1, Left[E, R], func(t1 T1) Either[E, R] { + return fold(e2, Left[E, R], func(t2 T2) Either[E, R] { + return fold(e3, Left[E, R], func(t3 T3) Either[E, R] { + return f(t1, t2, t3) + }) + }) + }) +} + +// Swap changes the order of type parameters +func Swap[E, A any](val Either[E, A]) Either[A, E] { + return fold(val, Right[A, E], Left[A, E]) +} diff --git a/either/either_test.go b/either/either_test.go new file mode 100644 index 0000000..dc416ed --- /dev/null +++ b/either/either_test.go @@ -0,0 +1,96 @@ +package either + +import ( + "errors" + "testing" + + 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" +) + +func TestIsLeft(t *testing.T) { + err := errors.New("Some error") + withError := Left[error, string](err) + + assert.True(t, IsLeft(withError)) + assert.False(t, IsRight(withError)) +} + +func TestIsRight(t *testing.T) { + noError := Right[error]("Carsten") + + assert.True(t, IsRight(noError)) + assert.False(t, IsLeft(noError)) +} + +func TestMapEither(t *testing.T) { + + assert.Equal(t, F.Pipe1(Right[error]("abc"), Map[error](utils.StringLen)), Right[error](3)) + + val2 := F.Pipe1(Left[string, string]("s"), Map[string](utils.StringLen)) + exp2 := Left[string, int]("s") + + assert.Equal(t, val2, exp2) +} + +func TestUnwrapError(t *testing.T) { + a := "" + err := errors.New("Some error") + withError := Left[error, string](err) + + res, extracted := UnwrapError(withError) + assert.Equal(t, a, res) + assert.Equal(t, extracted, err) + +} + +func TestReduce(t *testing.T) { + + s := S.Semigroup() + + assert.Equal(t, "foobar", F.Pipe1(Right[string]("bar"), Reduce[string](s.Concat, "foo"))) + assert.Equal(t, "foo", F.Pipe1(Left[string, string]("bar"), Reduce[string](s.Concat, "foo"))) + +} + +func TestAp(t *testing.T) { + f := S.Size + + assert.Equal(t, Right[string](3), F.Pipe1(Right[string](f), Ap[string, string, int](Right[string]("abc")))) + assert.Equal(t, Left[string, int]("maError"), F.Pipe1(Right[string](f), Ap[string, string, int](Left[string, string]("maError")))) + assert.Equal(t, Left[string, int]("mabError"), F.Pipe1(Left[string, func(string) int]("mabError"), Ap[string, string, int](Left[string, string]("maError")))) +} + +func TestAlt(t *testing.T) { + assert.Equal(t, Right[string](1), F.Pipe1(Right[string](1), Alt(F.Constant(Right[string](2))))) + assert.Equal(t, Right[string](1), F.Pipe1(Right[string](1), Alt(F.Constant(Left[string, int]("a"))))) + assert.Equal(t, Right[string](2), F.Pipe1(Left[string, int]("b"), Alt(F.Constant(Right[string](2))))) + assert.Equal(t, Left[string, int]("b"), F.Pipe1(Left[string, int]("a"), Alt(F.Constant(Left[string, int]("b"))))) +} + +func TestChainFirst(t *testing.T) { + f := F.Flow2(S.Size, Right[string, int]) + + assert.Equal(t, Right[string]("abc"), F.Pipe1(Right[string]("abc"), ChainFirst(f))) + assert.Equal(t, Left[string, string]("maError"), F.Pipe1(Left[string, string]("maError"), ChainFirst(f))) +} + +func TestChainOptionK(t *testing.T) { + f := ChainOptionK[string, int, int](F.Constant("a"))(func(n int) O.Option[int] { + if n > 0 { + return O.Some(n) + } + return O.None[int]() + }) + assert.Equal(t, Right[string](1), f(Right[string](1))) + assert.Equal(t, Left[string, int]("a"), f(Right[string](-1))) + assert.Equal(t, Left[string, int]("b"), f(Left[string, int]("b"))) +} + +func TestFromOption(t *testing.T) { + assert.Equal(t, Left[string, int]("none"), FromOption[string, int](F.Constant("none"))(O.None[int]())) + assert.Equal(t, Right[string](1), FromOption[string, int](F.Constant("none"))(O.Some(1))) +} diff --git a/either/eq.go b/either/eq.go new file mode 100644 index 0000000..9eda518 --- /dev/null +++ b/either/eq.go @@ -0,0 +1,25 @@ +package either + +import ( + EQ "github.com/ibm/fp-go/eq" + F "github.com/ibm/fp-go/function" +) + +// Constructs an equal predicate for an `Either` +func Eq[E, A any](e EQ.Eq[E], a EQ.Eq[A]) EQ.Eq[Either[E, A]] { + // some convenient shortcuts + eqa := F.Curry2(a.Equals) + eqe := F.Curry2(e.Equals) + + fca := F.Bind2nd(Fold[E, A, bool], F.Constant1[A](false)) + fce := F.Bind1st(Fold[E, A, bool], F.Constant1[E](false)) + + fld := Fold(F.Flow2(eqe, fca), F.Flow2(eqa, fce)) + + return EQ.FromEquals(F.Uncurry2(fld)) +} + +// FromStrictEquals constructs an `Eq` from the canonical comparison function +func FromStrictEquals[E, A comparable]() EQ.Eq[Either[E, A]] { + return Eq(EQ.FromStrictEquals[E](), EQ.FromStrictEquals[A]()) +} diff --git a/either/eq_test.go b/either/eq_test.go new file mode 100644 index 0000000..314a92f --- /dev/null +++ b/either/eq_test.go @@ -0,0 +1,30 @@ +package either + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEq(t *testing.T) { + + r1 := Of[string](1) + r2 := Of[string](1) + r3 := Of[string](2) + + e1 := Left[string, int]("a") + e2 := Left[string, int]("a") + e3 := Left[string, int]("b") + + eq := FromStrictEquals[string, int]() + + assert.True(t, eq.Equals(r1, r1)) + assert.True(t, eq.Equals(r1, r2)) + assert.False(t, eq.Equals(r1, r3)) + assert.False(t, eq.Equals(r1, e1)) + + assert.True(t, eq.Equals(e1, e1)) + assert.True(t, eq.Equals(e1, e2)) + assert.False(t, eq.Equals(e1, e3)) + assert.False(t, eq.Equals(e2, r2)) +} diff --git a/either/exec/exec.go b/either/exec/exec.go new file mode 100644 index 0000000..a52e595 --- /dev/null +++ b/either/exec/exec.go @@ -0,0 +1,23 @@ +package Exec + +import ( + "context" + + E "github.com/ibm/fp-go/either" + "github.com/ibm/fp-go/exec" + F "github.com/ibm/fp-go/function" + GE "github.com/ibm/fp-go/internal/exec" +) + +var ( + // Command executes a command + // use this version if the command does not produce any side effect, i.e. if the output is uniquely determined by by the input + // typically you'd rather use the IOEither version of the command + Command = F.Curry3(command) +) + +func command(name string, args []string, in []byte) E.Either[error, exec.CommandOutput] { + return E.TryCatchError(func() (exec.CommandOutput, error) { + return GE.Exec(context.Background(), name, args, in) + }) +} diff --git a/either/http/request.go b/either/http/request.go new file mode 100644 index 0000000..c9556de --- /dev/null +++ b/either/http/request.go @@ -0,0 +1,36 @@ +package Http + +import ( + "bytes" + "net/http" + + E "github.com/ibm/fp-go/either" +) + +var ( + PostRequest = bodyRequest("POST") + PutRequest = bodyRequest("PUT") + + GetRequest = noBodyRequest("GET") + DeleteRequest = noBodyRequest("DELETE") + OptionsRequest = noBodyRequest("OPTIONS") + HeadRequest = noBodyRequest("HEAD") +) + +func bodyRequest(method string) func(string) func([]byte) E.Either[error, *http.Request] { + return func(url string) func([]byte) E.Either[error, *http.Request] { + return func(body []byte) E.Either[error, *http.Request] { + return E.TryCatchError(func() (*http.Request, error) { + return http.NewRequest(method, url, bytes.NewReader(body)) + }) + } + } +} + +func noBodyRequest(method string) func(string) E.Either[error, *http.Request] { + return func(url string) E.Either[error, *http.Request] { + return E.TryCatchError(func() (*http.Request, error) { + return http.NewRequest(method, url, nil) + }) + } +} diff --git a/either/legacy.go b/either/legacy.go new file mode 100644 index 0000000..fd12f08 --- /dev/null +++ b/either/legacy.go @@ -0,0 +1,55 @@ +package either + +import "fmt" + +type EitherTag int + +const ( + LeftTag EitherTag = iota + RightTag +) + +// Either defines a data structure that logically holds either an E or an A. The tag discriminates the cases +type Either[E, A any] struct { + Tag EitherTag + Left E + Right A +} + +// String prints some debug info for the object +func (s Either[E, A]) String() string { + switch s.Tag { + case LeftTag: + return fmt.Sprintf("Left[%T, %T](%v)", s.Left, s.Right, s.Left) + case RightTag: + return fmt.Sprintf("Right[%T, %T](%v)", s.Left, s.Right, s.Right) + } + return "Invalid" +} + +func IsLeft[E, A any](val Either[E, A]) bool { + return val.Tag == LeftTag +} + +func IsRight[E, A any](val Either[E, A]) bool { + return val.Tag == RightTag +} + +func Left[E, A any](value E) Either[E, A] { + return Either[E, A]{Tag: LeftTag, Left: value} +} + +func Right[E, A any](value A) Either[E, A] { + return Either[E, A]{Tag: RightTag, Right: value} +} + +func fold[E, A, B any](ma Either[E, A], onLeft func(e E) B, onRight func(a A) B) B { + if IsLeft(ma) { + return onLeft(ma.Left) + } + return onRight(ma.Right) +} + +func Unwrap[E, A any](ma Either[E, A]) (A, E) { + return ma.Right, ma.Left +} diff --git a/either/logger.go b/either/logger.go new file mode 100644 index 0000000..8dc6824 --- /dev/null +++ b/either/logger.go @@ -0,0 +1,33 @@ +package either + +import ( + "log" + + F "github.com/ibm/fp-go/function" + L "github.com/ibm/fp-go/logging" +) + +func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) func(Either[E, A]) Either[E, A] { + return Fold( + func(e E) Either[E, A] { + left("%s: %v", prefix, e) + return Left[E, A](e) + }, + func(a A) Either[E, A] { + right("%s: %v", prefix, a) + return Right[E](a) + }) +} + +func Logger[E, A any](loggers ...*log.Logger) func(string) func(Either[E, A]) Either[E, A] { + left, right := L.LoggingCallbacks(loggers...) + return func(prefix string) func(Either[E, A]) Either[E, A] { + delegate := _log[E, A](left, right, prefix) + return func(ma Either[E, A]) Either[E, A] { + return F.Pipe1( + delegate(ma), + ChainTo[E, A](ma), + ) + } + } +} diff --git a/either/logger_test.go b/either/logger_test.go new file mode 100644 index 0000000..1097d94 --- /dev/null +++ b/either/logger_test.go @@ -0,0 +1,22 @@ +package either + +import ( + "testing" + + F "github.com/ibm/fp-go/function" + "github.com/stretchr/testify/assert" +) + +func TestLogger(t *testing.T) { + + l := Logger[error, string]() + + r := Right[error]("test") + + res := F.Pipe1( + r, + l("out"), + ) + + assert.Equal(t, r, res) +} diff --git a/either/modern._go b/either/modern._go new file mode 100644 index 0000000..7cb0fff --- /dev/null +++ b/either/modern._go @@ -0,0 +1,69 @@ +//go:build disabled + +package either + +// Either will either be E or A +type Either[E, A any] interface { + fmt.Stringer +} + +type left[E any] struct { + e E +} + +func (left[E]) IsLeft() bool { + return true +} + +type right[A any] struct { + a A +} + +func (right[A]) IsLeft() bool { + return false +} + +func (l left[E]) String() string { + return fmt.Sprintf("Left[%v]", l.e) +} + +func (r right[A]) String() string { + return fmt.Sprintf("Right[%v]", r.a) +} + +func IsLeft[E, A any](val Either[E, A]) bool { + switch any(val).(type) { + case left[E]: + return true + default: + return false + } +} + +func IsRight[E, A any](val Either[E, A]) bool { + return !IsLeft(val) +} + +func Left[E, A any](value E) Either[E, A] { + return left[E]{e: value} +} + +func Right[E, A any](value A) Either[E, A] { + return right[A]{a: value} +} + +func fold[E, A, B any](ma Either[E, A], onLeft func(e E) B, onRight func(a A) B) B { + if IsLeft(ma) { + return onLeft(ma.(left[E]).e) + } + return onRight(ma.(right[A]).a) +} + +func Unwrap[E, A any](ma Either[E, A]) (A, E) { + if IsLeft(ma) { + var a A + return a, ma.(left[E]).e + } + var E e + return ma.(right[A]).a, e +} diff --git a/either/record.go b/either/record.go new file mode 100644 index 0000000..7c458d2 --- /dev/null +++ b/either/record.go @@ -0,0 +1,30 @@ +package either + +import ( + F "github.com/ibm/fp-go/function" + RR "github.com/ibm/fp-go/internal/record" +) + +// TraverseRecord transforms a record of options into an option of a record +func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, E, A, B any](f func(A) Either[E, B]) func(GA) Either[E, GB] { + return RR.Traverse[GA]( + Of[E, GB], + MonadMap[E, GB, func(B) GB], + MonadAp[E, B, GB], + f, + ) +} + +// TraverseRecord transforms a record of eithers into an either of a record +func TraverseRecord[K comparable, E, A, B any](f func(A) Either[E, B]) func(map[K]A) Either[E, map[K]B] { + return TraverseRecordG[map[K]A, map[K]B](f) +} + +func SequenceRecordG[GA ~map[K]A, GOA ~map[K]Either[E, A], K comparable, E, A any](ma GOA) Either[E, GA] { + return TraverseRecordG[GOA, GA](F.Identity[Either[E, A]])(ma) +} + +// SequenceRecord converts a homogeneous sequence of either into an either of sequence +func SequenceRecord[K comparable, E, A any](ma map[K]Either[E, A]) Either[E, map[K]A] { + return SequenceRecordG[map[K]A](ma) +} diff --git a/either/resource.go b/either/resource.go new file mode 100644 index 0000000..6958d19 --- /dev/null +++ b/either/resource.go @@ -0,0 +1,29 @@ +package either + +import ( + F "github.com/ibm/fp-go/function" +) + +// constructs a function that creates a resource, then operates on it and then releases the resource +func WithResource[E, R, A any](onCreate func() Either[E, R], onRelease func(R) Either[E, any]) func(func(R) Either[E, A]) Either[E, A] { + + return func(f func(R) Either[E, A]) Either[E, A] { + return MonadChain( + onCreate(), func(r R) Either[E, A] { + // run the code and make sure to release as quickly as possible + res := f(r) + released := onRelease(r) + // handle the errors + return fold( + res, + Left[E, A], + func(a A) Either[E, A] { + return F.Pipe1( + released, + MapTo[E, any](a), + ) + }) + }, + ) + } +} diff --git a/either/resource_test.go b/either/resource_test.go new file mode 100644 index 0000000..a68e92b --- /dev/null +++ b/either/resource_test.go @@ -0,0 +1,39 @@ +package either + +import ( + "os" + "testing" + + F "github.com/ibm/fp-go/function" + "github.com/stretchr/testify/assert" +) + +func TestWithResource(t *testing.T) { + onCreate := func() Either[error, *os.File] { + return TryCatchError(func() (*os.File, error) { + return os.CreateTemp("", "*") + }) + } + onDelete := F.Flow2( + func(f *os.File) Either[error, string] { + return TryCatchError(func() (string, error) { + return f.Name(), f.Close() + }) + }, + Chain(func(name string) Either[error, any] { + return TryCatchError(func() (any, error) { + return name, os.Remove(name) + }) + }), + ) + + onHandler := func(f *os.File) Either[error, string] { + return Of[error](f.Name()) + } + + tempFile := WithResource[error, *os.File, string](onCreate, onDelete) + + resE := tempFile(onHandler) + + assert.True(t, IsRight(resE)) +} diff --git a/either/sequence.go b/either/sequence.go new file mode 100644 index 0000000..a8c8224 --- /dev/null +++ b/either/sequence.go @@ -0,0 +1,45 @@ +package either + +import ( + Apply "github.com/ibm/fp-go/apply" + T "github.com/ibm/fp-go/tuple" +) + +// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple + +func SequenceT1[E, A any](a Either[E, A]) Either[E, T.Tuple1[A]] { + return Apply.SequenceT1( + MonadMap[E, A, T.Tuple1[A]], + a, + ) +} + +func SequenceT2[E, A, B any](a Either[E, A], b Either[E, B]) Either[E, T.Tuple2[A, B]] { + return Apply.SequenceT2( + MonadMap[E, A, func(B) T.Tuple2[A, B]], + MonadAp[E, B, T.Tuple2[A, B]], + + a, b, + ) +} + +func SequenceT3[E, A, B, C any](a Either[E, A], b Either[E, B], c Either[E, C]) Either[E, T.Tuple3[A, B, C]] { + return Apply.SequenceT3( + MonadMap[E, A, func(B) func(C) T.Tuple3[A, B, C]], + MonadAp[E, B, func(C) T.Tuple3[A, B, C]], + MonadAp[E, C, T.Tuple3[A, B, C]], + + a, b, c, + ) +} + +func SequenceT4[E, A, B, C, D any](a Either[E, A], b Either[E, B], c Either[E, C], d Either[E, D]) Either[E, T.Tuple4[A, B, C, D]] { + return Apply.SequenceT4( + MonadMap[E, A, func(B) func(C) func(D) T.Tuple4[A, B, C, D]], + MonadAp[E, B, func(C) func(D) T.Tuple4[A, B, C, D]], + MonadAp[E, C, func(D) T.Tuple4[A, B, C, D]], + MonadAp[E, D, T.Tuple4[A, B, C, D]], + + a, b, c, d, + ) +} diff --git a/either/testing/laws.go b/either/testing/laws.go new file mode 100644 index 0000000..0782cd0 --- /dev/null +++ b/either/testing/laws.go @@ -0,0 +1,60 @@ +package testing + +import ( + "testing" + + ET "github.com/ibm/fp-go/either" + EQ "github.com/ibm/fp-go/eq" + L "github.com/ibm/fp-go/internal/monad/testing" +) + +// AssertLaws asserts the apply monad laws for the `Either` monad +func AssertLaws[E, A, B, C any](t *testing.T, + eqe EQ.Eq[E], + eqa EQ.Eq[A], + eqb EQ.Eq[B], + eqc EQ.Eq[C], + + ab func(A) B, + bc func(B) C, +) func(a A) bool { + + return L.AssertLaws(t, + ET.Eq(eqe, eqa), + ET.Eq(eqe, eqb), + ET.Eq(eqe, eqc), + + ET.Of[E, A], + ET.Of[E, B], + ET.Of[E, C], + + ET.Of[E, func(A) A], + ET.Of[E, func(A) B], + ET.Of[E, func(B) C], + ET.Of[E, func(func(A) B) B], + + ET.MonadMap[E, A, A], + ET.MonadMap[E, A, B], + ET.MonadMap[E, A, C], + ET.MonadMap[E, B, C], + + ET.MonadMap[E, func(B) C, func(func(A) B) func(A) C], + + ET.MonadChain[E, A, A], + ET.MonadChain[E, A, B], + ET.MonadChain[E, A, C], + ET.MonadChain[E, B, C], + + ET.MonadAp[E, A, A], + ET.MonadAp[E, A, B], + ET.MonadAp[E, B, C], + ET.MonadAp[E, A, C], + + ET.MonadAp[E, func(A) B, B], + ET.MonadAp[E, func(A) B, func(A) C], + + ab, + bc, + ) + +} diff --git a/either/testing/laws_test.go b/either/testing/laws_test.go new file mode 100644 index 0000000..d3a35c8 --- /dev/null +++ b/either/testing/laws_test.go @@ -0,0 +1,33 @@ +package testing + +import ( + "fmt" + "testing" + + EQ "github.com/ibm/fp-go/eq" + "github.com/stretchr/testify/assert" +) + +func TestMonadLaws(t *testing.T) { + // some comparison + eqe := EQ.FromStrictEquals[string]() + eqa := EQ.FromStrictEquals[bool]() + eqb := EQ.FromStrictEquals[int]() + eqc := EQ.FromStrictEquals[string]() + + ab := func(a bool) int { + if a { + return 1 + } + return 0 + } + + bc := func(b int) string { + return fmt.Sprintf("value %d", b) + } + + laws := AssertLaws(t, eqe, eqa, eqb, eqc, ab, bc) + + assert.True(t, laws(true)) + assert.True(t, laws(false)) +} diff --git a/either/traverse.go b/either/traverse.go new file mode 100644 index 0000000..fb242d7 --- /dev/null +++ b/either/traverse.go @@ -0,0 +1,54 @@ +package either + +import ( + F "github.com/ibm/fp-go/function" +) + +/* +* +We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces + +HKTRB = HKT +HKTA = HKT +HKTB = HKT +*/ +func traverse[E, A, B, HKTA, HKTB, HKTRB any]( + _of func(Either[E, B]) HKTRB, + _map func(HKTB, func(B) Either[E, B]) HKTRB, +) func(Either[E, A], func(A) HKTB) HKTRB { + + left := F.Flow2(Left[E, B], _of) + right := F.Bind2nd(_map, Right[E, B]) + + return func(ta Either[E, A], f func(A) HKTB) HKTRB { + return fold(ta, + left, + F.Flow2(f, right), + ) + } +} + +func Traverse[E, A, B, HKTA, HKTB, HKTRB any]( + _of func(Either[E, B]) HKTRB, + _map func(HKTB, func(B) Either[E, B]) HKTRB, +) func(func(A) HKTB) func(Either[E, A]) HKTRB { + delegate := traverse[E, A, B, HKTA](_of, _map) + return func(f func(A) HKTB) func(Either[E, A]) HKTRB { + return F.Bind2nd(delegate, f) + } +} + +/* +* +We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces + +HKTRA = HKT +HKTA = HKT +HKTB = HKT +*/ +func Sequence[E, A, HKTA, HKTRA any]( + _of func(Either[E, A]) HKTRA, + _map func(HKTA, func(A) Either[E, A]) HKTRA, +) func(Either[E, HKTA]) HKTRA { + return Fold(F.Flow2(Left[E, A], _of), F.Bind2nd(_map, Right[E, A])) +} diff --git a/either/traverse_test.go b/either/traverse_test.go new file mode 100644 index 0000000..51e2f6d --- /dev/null +++ b/either/traverse_test.go @@ -0,0 +1,38 @@ +package either + +import ( + "testing" + + F "github.com/ibm/fp-go/function" + O "github.com/ibm/fp-go/option" + "github.com/stretchr/testify/assert" +) + +func TestTraverse(t *testing.T) { + f := func(n int) O.Option[int] { + if n >= 2 { + return O.Of(n) + } + return O.None[int]() + } + trav := Traverse[string, int, int, O.Option[Either[string, int]]]( + O.Of[Either[string, int]], + O.MonadMap[int, Either[string, int]], + )(f) + + assert.Equal(t, O.Of(Left[string, int]("a")), F.Pipe1(Left[string, int]("a"), trav)) + assert.Equal(t, O.None[Either[string, int]](), F.Pipe1(Right[string](1), trav)) + assert.Equal(t, O.Of(Right[string](3)), F.Pipe1(Right[string](3), trav)) +} + +func TestSequence(t *testing.T) { + + seq := Sequence( + O.Of[Either[string, int]], + O.MonadMap[int, Either[string, int]], + ) + + assert.Equal(t, O.Of(Right[string](1)), seq(Right[string](O.Of(1)))) + assert.Equal(t, O.Of(Left[string, int]("a")), seq(Left[string, O.Option[int]]("a"))) + assert.Equal(t, O.None[Either[string, int]](), seq(Right[string](O.None[int]()))) +} diff --git a/either/variadic.go b/either/variadic.go new file mode 100644 index 0000000..c2975d8 --- /dev/null +++ b/either/variadic.go @@ -0,0 +1,81 @@ +package either + +func Variadic0[V, R any](f func([]V) (R, error)) func(...V) Either[error, R] { + return func(v ...V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(v) + }) + } +} + +func Variadic1[T1, V, R any](f func(T1, []V) (R, error)) func(T1, ...V) Either[error, R] { + return func(t1 T1, v ...V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, v) + }) + } +} + +func Variadic2[T1, T2, V, R any](f func(T1, T2, []V) (R, error)) func(T1, T2, ...V) Either[error, R] { + return func(t1 T1, t2 T2, v ...V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, v) + }) + } +} + +func Variadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, []V) (R, error)) func(T1, T2, T3, ...V) Either[error, R] { + return func(t1 T1, t2 T2, t3 T3, v ...V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, t3, v) + }) + } +} + +func Variadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, []V) (R, error)) func(T1, T2, T3, T4, ...V) Either[error, R] { + return func(t1 T1, t2 T2, t3 T3, t4 T4, v ...V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, t3, t4, v) + }) + } +} + +func Unvariadic0[V, R any](f func(...V) (R, error)) func([]V) Either[error, R] { + return func(v []V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(v...) + }) + } +} + +func Unvariadic1[T1, V, R any](f func(T1, ...V) (R, error)) func(T1, []V) Either[error, R] { + return func(t1 T1, v []V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, v...) + }) + } +} + +func Unvariadic2[T1, T2, V, R any](f func(T1, T2, ...V) (R, error)) func(T1, T2, []V) Either[error, R] { + return func(t1 T1, t2 T2, v []V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, v...) + }) + } +} + +func Unvariadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, ...V) (R, error)) func(T1, T2, T3, []V) Either[error, R] { + return func(t1 T1, t2 T2, t3 T3, v []V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, t3, v...) + }) + } +} + +func Unvariadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, ...V) (R, error)) func(T1, T2, T3, T4, []V) Either[error, R] { + return func(t1 T1, t2 T2, t3 T3, t4 T4, v []V) Either[error, R] { + return TryCatchError(func() (R, error) { + return f(t1, t2, t3, t4, v...) + }) + } +} diff --git a/eq/contramap.go b/eq/contramap.go new file mode 100644 index 0000000..5295d94 --- /dev/null +++ b/eq/contramap.go @@ -0,0 +1,11 @@ +package eq + +// Contramap implements an Equals predicate based on a mapping +func Contramap[A, B any](f func(b B) A) func(Eq[A]) Eq[B] { + return func(fa Eq[A]) Eq[B] { + equals := fa.Equals + return FromEquals(func(x, y B) bool { + return equals(f(x), f(y)) + }) + } +} diff --git a/eq/eq.go b/eq/eq.go new file mode 100644 index 0000000..019479a --- /dev/null +++ b/eq/eq.go @@ -0,0 +1,43 @@ +package eq + +import ( + F "github.com/ibm/fp-go/function" +) + +type Eq[T any] interface { + Equals(x, y T) bool +} + +type eq[T any] struct { + c func(x, y T) bool +} + +func (self eq[T]) Equals(x, y T) bool { + return self.c(x, y) +} + +func strictEq[A comparable](a, b A) bool { + return a == b +} + +// FromStrictEquals constructs an `Eq` from the canonical comparison function +func FromStrictEquals[T comparable]() Eq[T] { + return FromEquals(strictEq[T]) +} + +// FromEquals constructs an `Eq` from the comparison function +func FromEquals[T any](c func(x, y T) bool) Eq[T] { + return eq[T]{c: c} +} + +// Empty returns the equals predicate that is always true +func Empty[T any]() Eq[T] { + return FromEquals(F.Constant2[T, T](true)) +} + +// Equals returns a predicate to test if one value equals the other under an equals predicate +func Equals[T any](eq Eq[T]) func(T) func(T) bool { + return func(other T) func(T) bool { + return F.Bind2nd(eq.Equals, other) + } +} diff --git a/eq/monoid.go b/eq/monoid.go new file mode 100644 index 0000000..9e221c9 --- /dev/null +++ b/eq/monoid.go @@ -0,0 +1,18 @@ +package eq + +import ( + M "github.com/ibm/fp-go/monoid" + S "github.com/ibm/fp-go/semigroup" +) + +func Semigroup[A any]() S.Semigroup[Eq[A]] { + return S.MakeSemigroup(func(x, y Eq[A]) Eq[A] { + return FromEquals(func(a, b A) bool { + return x.Equals(a, b) && y.Equals(a, b) + }) + }) +} + +func Monoid[A any]() M.Monoid[Eq[A]] { + return M.MakeMonoid(Semigroup[A]().Concat, Empty[A]()) +} diff --git a/eq/testing/eq.go b/eq/testing/eq.go new file mode 100644 index 0000000..4f7d81d --- /dev/null +++ b/eq/testing/eq.go @@ -0,0 +1,13 @@ +package testing + +import ( + EQ "github.com/ibm/fp-go/eq" + "github.com/stretchr/testify/assert" +) + +// Eq implements the equal operation based on `ObjectsAreEqualValues` from the assertion library +func Eq[A any]() EQ.Eq[A] { + return EQ.FromEquals(func(l, r A) bool { + return assert.ObjectsAreEqualValues(l, r) + }) +} diff --git a/errors/conv.go b/errors/conv.go new file mode 100644 index 0000000..0e88c32 --- /dev/null +++ b/errors/conv.go @@ -0,0 +1,16 @@ +package errors + +import ( + "errors" + + O "github.com/ibm/fp-go/option" +) + +// As tries to extract the error of desired type from the given error +func As[A error]() func(error) O.Option[A] { + return O.FromValidation(func(err error) (A, bool) { + var a A + ok := errors.As(err, &a) + return a, ok + }) +} diff --git a/errors/conv_test.go b/errors/conv_test.go new file mode 100644 index 0000000..4fbfdd0 --- /dev/null +++ b/errors/conv_test.go @@ -0,0 +1,39 @@ +package errors + +import ( + "fmt" + "testing" + + F "github.com/ibm/fp-go/function" + O "github.com/ibm/fp-go/option" + "github.com/stretchr/testify/assert" +) + +type MyError struct{} + +func (m *MyError) Error() string { + return "boom" +} + +func TestAs(t *testing.T) { + root := &MyError{} + err := fmt.Errorf("This is my custom error, %w", root) + + errO := F.Pipe1( + err, + As[*MyError](), + ) + + assert.Equal(t, O.Of(root), errO) +} + +func TestNotAs(t *testing.T) { + err := fmt.Errorf("This is my custom error") + + errO := F.Pipe1( + err, + As[*MyError](), + ) + + assert.Equal(t, O.None[*MyError](), errO) +} diff --git a/errors/identity.go b/errors/identity.go new file mode 100644 index 0000000..2e11e69 --- /dev/null +++ b/errors/identity.go @@ -0,0 +1,7 @@ +package errors + +import ( + F "github.com/ibm/fp-go/function" +) + +var IdentityError = F.Identity[error] diff --git a/errors/message_test.go b/errors/message_test.go new file mode 100644 index 0000000..44cf3ac --- /dev/null +++ b/errors/message_test.go @@ -0,0 +1,18 @@ +package errors + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOnError(t *testing.T) { + onError := OnError("Failed to process [%s]", "filename") + + err := fmt.Errorf("Cause") + + err1 := onError(err) + + assert.Equal(t, "Failed to process [filename], Caused By: Cause", err1.Error()) +} diff --git a/errors/messages.go b/errors/messages.go new file mode 100644 index 0000000..553d0b0 --- /dev/null +++ b/errors/messages.go @@ -0,0 +1,23 @@ +package errors + +import ( + "fmt" + + A "github.com/ibm/fp-go/array" +) + +// OnNone generates a nullary function that produces a formatted error +func OnNone(msg string, args ...any) func() error { + return func() error { + return fmt.Errorf(msg, args...) + } +} + +// OnError generates a unary function that produces a formatted error. The argument +// to that function is the root cause of the error and the message will be augmented with +// a format string containing %w +func OnError(msg string, args ...any) func(error) error { + return func(err error) error { + return fmt.Errorf(msg+", Caused By: %w", A.ArrayConcatAll(args, A.Of[any](err))...) + } +} diff --git a/exec/exec.go b/exec/exec.go new file mode 100644 index 0000000..b196292 --- /dev/null +++ b/exec/exec.go @@ -0,0 +1,15 @@ +package exec + +import ( + T "github.com/ibm/fp-go/tuple" +) + +type ( + // command output + CommandOutput = T.Tuple2[[]byte, []byte] +) + +var ( + StdOut = T.First[[]byte, []byte] + StdErr = T.Second[[]byte, []byte] +) diff --git a/fp-go.exe b/fp-go.exe new file mode 100644 index 0000000..b69c035 Binary files /dev/null and b/fp-go.exe differ diff --git a/function/bind.go b/function/bind.go new file mode 100644 index 0000000..669ccfc --- /dev/null +++ b/function/bind.go @@ -0,0 +1,52 @@ +package function + +func Bind1st[T1, T2, R any](f func(T1, T2) R, t1 T1) func(T2) R { + return func(t2 T2) R { + return f(t1, t2) + } +} +func Bind2nd[T1, T2, R any](f func(T1, T2) R, t2 T2) func(T1) R { + return func(t1 T1) R { + return f(t1, t2) + } +} + +func Bind1[T1, R any](f func(T1) R, t1 T1) func() R { + return func() R { + return f(t1) + } +} + +func Bind2[T1, T2, R any](f func(T1, T2) R, t1 T1, t2 T2) func() R { + return func() R { + return f(t1, t2) + } +} + +func Bind3[T1, T2, T3, R any](f func(T1, T2, T3) R, t1 T1, t2 T2, t3 T3) func() R { + return func() R { + return f(t1, t2, t3) + } +} + +func SK[T1, T2 any](_ T1, t2 T2) T2 { + return t2 +} + +func Ignore1[T, R any](f func() R) func(T) R { + return func(_ T) R { + return f() + } +} + +func Ignore1st[T1, T2, R any](f func(T2) R) func(T1, T2) R { + return func(_ T1, t2 T2) R { + return f(t2) + } +} + +func Ignore2nd[T1, T2, R any](f func(T1) R) func(T1, T2) R { + return func(t1 T1, _ T2) R { + return f(t1) + } +} diff --git a/function/constants.go b/function/constants.go new file mode 100644 index 0000000..d48fb91 --- /dev/null +++ b/function/constants.go @@ -0,0 +1,9 @@ +package function + +var ConstTrue = Constant(true) +var ConstFalse = Constant(false) + +// ConstNil returns nil +func ConstNil[A any]() *A { + return (*A)(nil) +} diff --git a/function/curry.go b/function/curry.go new file mode 100644 index 0000000..e644b66 --- /dev/null +++ b/function/curry.go @@ -0,0 +1,77 @@ +package function + +func Curry1[T1, R any](f func(T1) R) func(T1) R { + return f +} + +func Curry2[T1, T2, R any](f func(T1, T2) R) func(T1) func(T2) R { + return func(t1 T1) func(T2) R { + return func(t2 T2) R { + return f(t1, t2) + } + } +} + +func Curry3[T1, T2, T3, R any](f func(T1, T2, T3) R) func(T1) func(T2) func(T3) R { + return func(t1 T1) func(T2) func(T3) R { + return func(t2 T2) func(T3) R { + return func(t3 T3) R { + return f(t1, t2, t3) + } + } + } +} + +func Curry4[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) R) func(T1) func(T2) func(T3) func(T4) R { + return func(t1 T1) func(T2) func(T3) func(T4) R { + return func(t2 T2) func(T3) func(T4) R { + return func(t3 T3) func(T4) R { + return func(t4 T4) R { + return f(t1, t2, t3, t4) + } + } + } + } +} + +func Curry5[T1, T2, T3, T4, T5, R any](f func(T1, T2, T3, T4, T5) R) func(T1) func(T2) func(T3) func(T4) func(T5) R { + return func(t1 T1) func(T2) func(T3) func(T4) func(T5) R { + return func(t2 T2) func(T3) func(T4) func(T5) R { + return func(t3 T3) func(T4) func(T5) R { + return func(t4 T4) func(T5) R { + return func(t5 T5) R { + return f(t1, t2, t3, t4, t5) + } + } + } + } + } +} + +func Uncurry1[T1, R any](f func(T1) R) func(T1) R { + return f +} + +func Uncurry2[T1, T2, R any](f func(T1) func(T2) R) func(T1, T2) R { + return func(t1 T1, t2 T2) R { + return f(t1)(t2) + } +} + +func Uncurry3[T1, T2, T3, R any](f func(T1) func(T2) func(T3) R) func(T1, T2, T3) R { + return func(t1 T1, t2 T2, t3 T3) R { + return f(t1)(t2)(t3) + } +} + +func Uncurry4[T1, T2, T3, T4, R any](f func(T1) func(T2) func(T3) func(T4) R) func(T1, T2, T3, T4) R { + return func(t1 T1, t2 T2, t3 T3, t4 T4) R { + return f(t1)(t2)(t3)(t4) + } +} + +func Uncurry5[T1, T2, T3, T4, T5, R any](f func(T1) func(T2) func(T3) func(T4) func(T5) R) func(T1, T2, T3, T4, T5) R { + return func(t1 T1, t2 T2, t3 T3, t4 T4, t5 T5) R { + return f(t1)(t2)(t3)(t4)(t5) + } +} diff --git a/function/function.go b/function/function.go new file mode 100644 index 0000000..8389344 --- /dev/null +++ b/function/function.go @@ -0,0 +1,218 @@ +package function + +// what a mess, golang does not have proper types ... + +func Pipe1[A, R any](a A, f1 func(a A) R) R { + // return f1(a) + r1 := f1(a) + return r1 +} + +func Pipe2[A, T1, R any](a A, f1 func(a A) T1, f2 func(t1 T1) R) R { + // return f2(f1(a)) + r1 := f1(a) + r2 := f2(r1) + return r2 +} + +func Pipe3[A, T1, T2, R any](a A, f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) R) R { + // return f3(f2(f1(a))) + r1 := f1(a) + r2 := f2(r1) + r3 := f3(r2) + return r3 +} + +func Pipe4[A, T1, T2, T3, R any](a A, f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) R) R { + // return f4(f3(f2(f1(a)))) + r1 := f1(a) + r2 := f2(r1) + r3 := f3(r2) + r4 := f4(r3) + return r4 +} + +func Pipe5[A, T1, T2, T3, T4, R any](a A, f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) R) R { + // return f5(f4(f3(f2(f1(a))))) + r1 := f1(a) + r2 := f2(r1) + r3 := f3(r2) + r4 := f4(r3) + r5 := f5(r4) + return r5 +} + +func Pipe6[A, T1, T2, T3, T4, T5, R any](a A, f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) R) R { + // return f6(f5(f4(f3(f2(f1(a)))))) + r1 := f1(a) + r2 := f2(r1) + r3 := f3(r2) + r4 := f4(r3) + r5 := f5(r4) + r6 := f6(r5) + return r6 +} + +func Pipe7[A, T1, T2, T3, T4, T5, T6, R any](a A, f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) T6, f7 func(t6 T6) R) R { + // return f7(f6(f5(f4(f3(f2(f1(a))))))) + r1 := f1(a) + r2 := f2(r1) + r3 := f3(r2) + r4 := f4(r3) + r5 := f5(r4) + r6 := f6(r5) + r7 := f7(r6) + return r7 +} + +func Pipe8[A, T1, T2, T3, T4, T5, T6, T7, R any](a A, f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) T6, f7 func(t6 T6) T7, f8 func(t7 T7) R) R { + // return f8(f7(f6(f5(f4(f3(f2(f1(a)))))))) + r1 := f1(a) + r2 := f2(r1) + r3 := f3(r2) + r4 := f4(r3) + r5 := f5(r4) + r6 := f6(r5) + r7 := f7(r6) + r8 := f8(r7) + return r8 +} + +func Pipe9[A, T1, T2, T3, T4, T5, T6, T7, T8, R any](a A, f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) T6, f7 func(t6 T6) T7, f8 func(t7 T7) T8, f9 func(t8 T8) R) R { + // return f9(f8(f7(f6(f5(f4(f3(f2(f1(a))))))))) + r1 := f1(a) + r2 := f2(r1) + r3 := f3(r2) + r4 := f4(r3) + r5 := f5(r4) + r6 := f6(r5) + r7 := f7(r6) + r8 := f8(r7) + r9 := f9(r8) + return r9 +} + +func Flow1[A, R any](f1 func(a A) R) func(a A) R { + return f1 +} + +func Flow2[A, T1, R any](f1 func(a A) T1, f2 func(t1 T1) R) func(a A) R { + return func(a A) R { + return Pipe2(a, f1, f2) + } +} + +func Flow3[A, T1, T2, R any](f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) R) func(a A) R { + return func(a A) R { + return Pipe3(a, f1, f2, f3) + } +} + +func Flow4[A, T1, T2, T3, R any](f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) R) func(a A) R { + return func(a A) R { + return Pipe4(a, f1, f2, f3, f4) + } +} + +func Flow5[A, T1, T2, T3, T4, R any](f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) R) func(a A) R { + return func(a A) R { + return Pipe5(a, f1, f2, f3, f4, f5) + } +} + +func Flow6[A, T1, T2, T3, T4, T5, R any](f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) R) func(a A) R { + return func(a A) R { + return Pipe6(a, f1, f2, f3, f4, f5, f6) + } +} + +func Flow7[A, T1, T2, T3, T4, T5, T6, R any](f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) T6, f7 func(t6 T6) R) func(a A) R { + return func(a A) R { + return Pipe7(a, f1, f2, f3, f4, f5, f6, f7) + } +} + +func Flow8[A, T1, T2, T3, T4, T5, T6, T7, R any](f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) T6, f7 func(t6 T6) T7, f8 func(t7 T7) R) func(a A) R { + return func(a A) R { + return Pipe8(a, f1, f2, f3, f4, f5, f6, f7, f8) + } +} + +func Flow9[A, T1, T2, T3, T4, T5, T6, T7, T8, R any](f1 func(a A) T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) T4, f5 func(t4 T4) T5, f6 func(t5 T5) T6, f7 func(t6 T6) T7, f8 func(t7 T7) T8, f9 func(t8 T8) R) func(a A) R { + return func(a A) R { + return Pipe9(a, f1, f2, f3, f4, f5, f6, f7, f8, f9) + } +} + +// Identity returns the value 'a' +func Identity[A any](a A) A { + return a +} + +// Constant creates a nullary function that returns the constant value 'a' +func Constant[A any](a A) func() A { + return func() A { + return a + } +} + +// Constant1 creates a unary function that returns the constant value 'a' and ignores its input +func Constant1[B, A any](a A) func(B) A { + return func(_ B) A { + return a + } +} + +// Constant2 creates a binary function that returns the constant value 'a' and ignores its inputs +func Constant2[B, C, A any](a A) func(B, C) A { + return func(_ B, _ C) A { + return a + } +} + +func IsNil[A any](a *A) bool { + return a == nil +} + +func IsNonNil[A any](a *A) bool { + return a != nil +} + +// Swap returns a new binary function that changes the order of input parameters +func Swap[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R { + return func(t2 T2, t1 T1) R { + return f(t1, t2) + } +} + +// First returns the first out of two input values +func First[T1, T2 any](t1 T1, _ T2) T1 { + return t1 +} + +// Second returns the second out of two input values +func Second[T1, T2 any](_ T1, t2 T2) T2 { + return t2 +} + +func Nullary1[R any](f1 func() R) func() R { + return f1 +} + +func Nullary2[T1, R any](f1 func() T1, f2 func(t1 T1) R) func() R { + return func() R { + return Pipe1(f1(), f2) + } +} + +func Nullary3[T1, T2, R any](f1 func() T1, f2 func(t1 T1) T2, f3 func(t2 T2) R) func() R { + return func() R { + return Pipe2(f1(), f2, f3) + } +} + +func Nullary4[T1, T2, T3, R any](f1 func() T1, f2 func(t1 T1) T2, f3 func(t2 T2) T3, f4 func(t3 T3) R) func() R { + return func() R { + return Pipe3(f1(), f2, f3, f4) + } +} diff --git a/function/ref.go b/function/ref.go new file mode 100644 index 0000000..5887b66 --- /dev/null +++ b/function/ref.go @@ -0,0 +1,9 @@ +package function + +func Ref[A any](a A) *A { + return &a +} + +func Deref[A any](a *A) A { + return *a +} diff --git a/function/ternary.go b/function/ternary.go new file mode 100644 index 0000000..1a54e32 --- /dev/null +++ b/function/ternary.go @@ -0,0 +1,10 @@ +package function + +func Ternary[A, B any](pred func(A) bool, onTrue func(A) B, onFalse func(A) B) func(A) B { + return func(a A) B { + if pred(a) { + return onTrue(a) + } + return onFalse(a) + } +} diff --git a/function/type.go b/function/type.go new file mode 100644 index 0000000..bb0e80c --- /dev/null +++ b/function/type.go @@ -0,0 +1,5 @@ +package function + +func ToAny[A any](a A) any { + return any(a) +} diff --git a/function/variadic.go b/function/variadic.go new file mode 100644 index 0000000..c8acabc --- /dev/null +++ b/function/variadic.go @@ -0,0 +1,61 @@ +package function + +func Variadic0[V, R any](f func([]V) R) func(...V) R { + return func(v ...V) R { + return f(v) + } +} + +func Variadic1[T1, V, R any](f func(T1, []V) R) func(T1, ...V) R { + return func(t1 T1, v ...V) R { + return f(t1, v) + } +} + +func Variadic2[T1, T2, V, R any](f func(T1, T2, []V) R) func(T1, T2, ...V) R { + return func(t1 T1, t2 T2, v ...V) R { + return f(t1, t2, v) + } +} + +func Variadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, []V) R) func(T1, T2, T3, ...V) R { + return func(t1 T1, t2 T2, t3 T3, v ...V) R { + return f(t1, t2, t3, v) + } +} + +func Variadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, []V) R) func(T1, T2, T3, T4, ...V) R { + return func(t1 T1, t2 T2, t3 T3, t4 T4, v ...V) R { + return f(t1, t2, t3, t4, v) + } +} + +func Unvariadic0[V, R any](f func(...V) R) func([]V) R { + return func(v []V) R { + return f(v...) + } +} + +func Unvariadic1[T1, V, R any](f func(T1, ...V) R) func(T1, []V) R { + return func(t1 T1, v []V) R { + return f(t1, v...) + } +} + +func Unvariadic2[T1, T2, V, R any](f func(T1, T2, ...V) R) func(T1, T2, []V) R { + return func(t1 T1, t2 T2, v []V) R { + return f(t1, t2, v...) + } +} + +func Unvariadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, ...V) R) func(T1, T2, T3, []V) R { + return func(t1 T1, t2 T2, t3 T3, v []V) R { + return f(t1, t2, t3, v...) + } +} + +func Unvariadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, ...V) R) func(T1, T2, T3, T4, []V) R { + return func(t1 T1, t2 T2, t3 T3, t4 T4, v []V) R { + return f(t1, t2, t3, t4, v...) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d643ec0 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module github.com/ibm/fp-go + +go 1.20 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5bddba9 --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/applicative/testing/law.go b/internal/applicative/testing/law.go new file mode 100644 index 0000000..491bf09 --- /dev/null +++ b/internal/applicative/testing/law.go @@ -0,0 +1,126 @@ +package testing + +import ( + "testing" + + E "github.com/ibm/fp-go/eq" + F "github.com/ibm/fp-go/function" + L "github.com/ibm/fp-go/internal/apply/testing" + "github.com/stretchr/testify/assert" +) + +// Applicative identity law +// +// A.ap(A.of(a => a), fa) <-> fa +func AssertIdentity[HKTA, HKTAA, A any](t *testing.T, + eq E.Eq[HKTA], + + fof func(func(A) A) HKTAA, + + fap func(HKTAA, HKTA) HKTA, +) func(fa HKTA) bool { + return func(fa HKTA) bool { + + left := fap(fof(F.Identity[A]), fa) + right := fa + + return assert.True(t, eq.Equals(left, right), "Applicative identity") + } +} + +// Applicative homomorphism law +// +// A.ap(A.of(ab), A.of(a)) <-> A.of(ab(a)) +func AssertHomomorphism[HKTA, HKTB, HKTAB, A, B any](t *testing.T, + eq E.Eq[HKTB], + + fofa func(A) HKTA, + fofb func(B) HKTB, + fofab func(func(A) B) HKTAB, + + fap func(HKTAB, HKTA) HKTB, + + ab func(A) B, +) func(a A) bool { + return func(a A) bool { + + left := fap(fofab(ab), fofa(a)) + right := fofb(ab(a)) + + return assert.True(t, eq.Equals(left, right), "Applicative homomorphism") + } +} + +// Applicative interchange law +// +// A.ap(fab, A.of(a)) <-> A.ap(A.of(ab => ab(a)), fab) +func AssertInterchange[HKTA, HKTB, HKTAB, HKTABB, A, B any](t *testing.T, + eq E.Eq[HKTB], + + fofa func(A) HKTA, + fofb func(B) HKTB, + fofab func(func(A) B) HKTAB, + fofabb func(func(func(A) B) B) HKTABB, + + fapab func(HKTAB, HKTA) HKTB, + fapabb func(HKTABB, HKTAB) HKTB, + + ab func(A) B, +) func(a A) bool { + return func(a A) bool { + + fab := fofab(ab) + + left := fapab(fab, fofa(a)) + right := fapabb(fofabb(func(ab func(A) B) B { + return ab(a) + }), fab) + + return assert.True(t, eq.Equals(left, right), "Applicative homomorphism") + } +} + +// AssertLaws asserts the apply laws `identity`, `composition`, `associative composition`, 'applicative identity', 'homomorphism', 'interchange' +func AssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTABAC, A, B, C any](t *testing.T, + eqa E.Eq[HKTA], + eqb E.Eq[HKTB], + eqc E.Eq[HKTC], + + fofa func(A) HKTA, + fofb func(B) HKTB, + + fofaa func(func(A) A) HKTAA, + fofab func(func(A) B) HKTAB, + fofbc func(func(B) C) HKTBC, + fofabb func(func(func(A) B) B) HKTABB, + + faa func(HKTA, func(A) A) HKTA, + fab func(HKTA, func(A) B) HKTB, + fac func(HKTA, func(A) C) HKTC, + fbc func(HKTB, func(B) C) HKTC, + + fmap func(HKTBC, func(func(B) C) func(func(A) B) func(A) C) HKTABAC, + + fapaa func(HKTAA, HKTA) HKTA, + fapab func(HKTAB, HKTA) HKTB, + fapbc func(HKTBC, HKTB) HKTC, + fapac func(HKTAC, HKTA) HKTC, + + fapabb func(HKTABB, HKTAB) HKTB, + fapabac func(HKTABAC, HKTAB) HKTAC, + + ab func(A) B, + bc func(B) C, +) func(a A) bool { + // apply laws + apply := L.AssertLaws(t, eqa, eqc, fofab, fofbc, faa, fab, fac, fbc, fmap, fapab, fapbc, fapac, fapabac, ab, bc) + // applicative laws + identity := AssertIdentity(t, eqa, fofaa, fapaa) + homomorphism := AssertHomomorphism(t, eqb, fofa, fofb, fofab, fapab, ab) + interchange := AssertInterchange(t, eqb, fofa, fofb, fofab, fofabb, fapab, fapabb, ab) + + return func(a A) bool { + fa := fofa(a) + return apply(fa) && identity(fa) && homomorphism(a) && interchange(a) + } +} diff --git a/internal/apply/ap.go b/internal/apply/ap.go new file mode 100644 index 0000000..f08eb6e --- /dev/null +++ b/internal/apply/ap.go @@ -0,0 +1,94 @@ +package Apply + +import ( + F "github.com/ibm/fp-go/function" +) + +// HKTFGA = HKT> +// HKTFGB = HKT> +// HKTFGAB = HKT B>> + +// HKTGA = HKT +// HKTGB = HKT +// HKTGAB = HKT B> +func MonadAp[HKTGA, HKTGB, HKTGAB, HKTFGAB, HKTFGGAB, HKTFGA, HKTFGB any]( + fap func(HKTFGGAB, HKTFGA) HKTFGB, + fmap func(HKTFGAB, func(HKTGAB) func(HKTGA) HKTGB) HKTFGGAB, + gap func(HKTGAB, HKTGA) HKTGB, + + fab HKTFGAB, + fa HKTFGA) HKTFGB { + + return fap(fmap(fab, F.Bind1st(F.Bind1st[HKTGAB, HKTGA, HKTGB], gap)), fa) +} + +// export function ap( +// F: Apply, +// G: Apply +// ): (fa: HKT>) => (fab: HKT B>>) => HKT> { +// return (fa: HKT>) => (fab: HKT B>>): HKT> => +// F.ap( +// F.map(fab, (gab) => (ga: HKT) => G.ap(gab, ga)), +// fa +// ) +// } + +// function apFirst(A: Apply): (second: HKT) => (first: HKT) => HKT { +// return (second) => (first) => +// A.ap( +// A.map(first, (a) => () => a), +// second +// ) +// } + +// Functor.map: A>(fa: HKT, f: (a: A) => () => A) => HKT A> + +// Apply.ap: (fab: HKT A>, fa: HKT) => HKT + +func MonadApFirst[HKTGA, HKTGB, HKTGBA, A, B any]( + fap func(HKTGBA, HKTGB) HKTGA, + fmap func(HKTGA, func(A) func(B) A) HKTGBA, + + first HKTGA, + second HKTGB, +) HKTGA { + return fap( + fmap(first, F.Constant1[B, A]), + second, + ) +} + +func ApFirst[HKTGA, HKTGB, HKTGBA, A, B any]( + fap func(HKTGBA, HKTGB) HKTGA, + fmap func(HKTGA, func(A) func(B) A) HKTGBA, + + second HKTGB, +) func(HKTGA) HKTGA { + return func(first HKTGA) HKTGA { + return MonadApFirst(fap, fmap, first, second) + } +} + +func MonadApSecond[HKTGA, HKTGB, HKTGBB, A, B any]( + fap func(HKTGBB, HKTGB) HKTGB, + fmap func(HKTGA, func(A) func(B) B) HKTGBB, + + first HKTGA, + second HKTGB, +) HKTGB { + return fap( + fmap(first, F.Constant1[A](F.Identity[B])), + second, + ) +} + +func ApSecond[HKTGA, HKTGB, HKTGBB, A, B any]( + fap func(HKTGBB, HKTGB) HKTGB, + fmap func(HKTGA, func(A) func(B) B) HKTGBB, + + second HKTGB, +) func(HKTGA) HKTGB { + return func(first HKTGA) HKTGB { + return MonadApSecond(fap, fmap, first, second) + } +} diff --git a/internal/apply/testing/laws.go b/internal/apply/testing/laws.go new file mode 100644 index 0000000..7b0e29e --- /dev/null +++ b/internal/apply/testing/laws.go @@ -0,0 +1,82 @@ +package testing + +import ( + "testing" + + E "github.com/ibm/fp-go/eq" + FCT "github.com/ibm/fp-go/internal/functor/testing" + "github.com/stretchr/testify/assert" +) + +// Apply associative composition law +// +// F.ap(F.ap(F.map(fbc, bc => ab => a => bc(ab(a))), fab), fa) <-> F.ap(fbc, F.ap(fab, fa)) +func AssertAssociativeComposition[HKTA, HKTB, HKTC, HKTAB, HKTBC, HKTAC, HKTABAC, A, B, C any](t *testing.T, + eq E.Eq[HKTC], + + fofab func(func(A) B) HKTAB, + fofbc func(func(B) C) HKTBC, + + fmap func(HKTBC, func(func(B) C) func(func(A) B) func(A) C) HKTABAC, + + fapab func(HKTAB, HKTA) HKTB, + fapbc func(HKTBC, HKTB) HKTC, + fapac func(HKTAC, HKTA) HKTC, + + fapabac func(HKTABAC, HKTAB) HKTAC, + + ab func(A) B, + bc func(B) C, +) func(fa HKTA) bool { + return func(fa HKTA) bool { + + fab := fofab(ab) + fbc := fofbc(bc) + + left := fapac(fapabac(fmap(fbc, func(bc func(B) C) func(func(A) B) func(A) C { + return func(ab func(A) B) func(A) C { + return func(a A) C { + return bc(ab(a)) + } + } + }), fab), fa) + + right := fapbc(fbc, fapab(fab, fa)) + + return assert.True(t, eq.Equals(left, right), "Apply associative composition") + } +} + +// AssertLaws asserts the apply laws `identity`, `composition` and `associative composition` +func AssertLaws[HKTA, HKTB, HKTC, HKTAB, HKTBC, HKTAC, HKTABAC, A, B, C any](t *testing.T, + eqa E.Eq[HKTA], + eqc E.Eq[HKTC], + + fofab func(func(A) B) HKTAB, + fofbc func(func(B) C) HKTBC, + + faa func(HKTA, func(A) A) HKTA, + fab func(HKTA, func(A) B) HKTB, + fac func(HKTA, func(A) C) HKTC, + fbc func(HKTB, func(B) C) HKTC, + + fmap func(HKTBC, func(func(B) C) func(func(A) B) func(A) C) HKTABAC, + + fapab func(HKTAB, HKTA) HKTB, + fapbc func(HKTBC, HKTB) HKTC, + fapac func(HKTAC, HKTA) HKTC, + + fapabac func(HKTABAC, HKTAB) HKTAC, + + ab func(A) B, + bc func(B) C, +) func(fa HKTA) bool { + // functor laws + functor := FCT.AssertLaws(t, eqa, eqc, faa, fab, fac, fbc, ab, bc) + // associative composition laws + composition := AssertAssociativeComposition(t, eqc, fofab, fofbc, fmap, fapab, fapbc, fapac, fapabac, ab, bc) + + return func(fa HKTA) bool { + return functor(fa) && composition(fa) + } +} diff --git a/internal/array/array.go b/internal/array/array.go new file mode 100644 index 0000000..68dad58 --- /dev/null +++ b/internal/array/array.go @@ -0,0 +1,61 @@ +package array + +func Slice[GA ~[]A, A any](low, high int) func(as GA) GA { + return func(as GA) GA { + return as[low:high] + } +} + +func IsEmpty[GA ~[]A, A any](as GA) bool { + return len(as) == 0 +} + +func IsNil[GA ~[]A, A any](as GA) bool { + return as == nil +} + +func IsNonNil[GA ~[]A, A any](as GA) bool { + return as != nil +} + +func Reduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B { + current := initial + count := len(fa) + for i := 0; i < count; i++ { + current = f(current, fa[i]) + } + return current +} + +func Append[GA ~[]A, A any](as GA, a A) GA { + return append(as, a) +} + +func Empty[GA ~[]A, A any]() GA { + return make(GA, 0) +} + +func upsertAt[GA ~[]A, A any](fa GA, a A) GA { + buf := make(GA, len(fa)+1) + buf[copy(buf, fa)] = a + return buf +} + +func UpsertAt[GA ~[]A, A any](a A) func(GA) GA { + return func(ma GA) GA { + return upsertAt(ma, a) + } +} + +func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB { + count := len(as) + bs := make(GB, count) + for i := count - 1; i >= 0; i-- { + bs[i] = f(as[i]) + } + return bs +} + +func ConstNil[GA ~[]A, A any]() GA { + return (GA)(nil) +} diff --git a/internal/array/traverse.go b/internal/array/traverse.go new file mode 100644 index 0000000..b508664 --- /dev/null +++ b/internal/array/traverse.go @@ -0,0 +1,70 @@ +package array + +import ( + F "github.com/ibm/fp-go/function" +) + +/* +* +We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces + +HKTRB = HKT +HKTB = HKT +HKTAB = HKT +*/ +func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any]( + _of func(GB) HKTRB, + _map func(HKTRB, func(GB) func(B) GB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + ta GA, + f func(A) HKTB) HKTRB { + return MonadTraverseReduce(_of, _map, _ap, ta, f, Append[GB, B], Empty[GB]()) +} + +func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any]( + _of func(GB) HKTRB, + _map func(HKTRB, func(GB) func(B) GB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + f func(A) HKTB) func(GA) HKTRB { + + return func(ma GA) HKTRB { + return MonadTraverse(_of, _map, _ap, ma, f) + } +} + +func MonadTraverseReduce[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any]( + _of func(GB) HKTRB, + _map func(HKTRB, func(GB) func(B) GB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + ta GA, + + transform func(A) HKTB, + reduce func(GB, B) GB, + initial GB, +) HKTRB { + mmap := F.Bind2nd(_map, F.Curry2(reduce)) + + return Reduce(ta, func(r HKTRB, a A) HKTRB { + return _ap( + mmap(r), + transform(a), + ) + }, _of(initial)) +} + +func TraverseReduce[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any]( + _of func(GB) HKTRB, + _map func(HKTRB, func(GB) func(B) GB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + transform func(A) HKTB, + reduce func(GB, B) GB, + initial GB, +) func(GA) HKTRB { + return func(ta GA) HKTRB { + return MonadTraverseReduce(_of, _map, _ap, ta, transform, reduce, initial) + } +} diff --git a/internal/chain/chain.go b/internal/chain/chain.go new file mode 100644 index 0000000..a4136b0 --- /dev/null +++ b/internal/chain/chain.go @@ -0,0 +1,46 @@ +package chain + +import ( + F "github.com/ibm/fp-go/function" +) + +// HKTA=HKT[A] +// HKTB=HKT[B] +func MonadChainFirst[A, B, HKTA, HKTB any]( + mchain func(HKTA, func(A) HKTA) HKTA, + mmap func(HKTB, func(B) A) HKTA, + first HKTA, + f func(A) HKTB, +) HKTA { + return mchain(first, func(a A) HKTA { + return mmap(f(a), F.Constant1[B](a)) + }) +} + +func MonadChain[A, B, HKTA, HKTB any]( + mchain func(HKTA, func(A) HKTB) HKTB, + first HKTA, + f func(A) HKTB, +) HKTB { + return mchain(first, f) +} + +// HKTA=HKT[A] +// HKTB=HKT[B] +func ChainFirst[A, B, HKTA, HKTB any]( + mchain func(HKTA, func(A) HKTA) HKTA, + mmap func(HKTB, func(B) A) HKTA, + f func(A) HKTB) func(HKTA) HKTA { + return F.Bind2nd(mchain, func(a A) HKTA { + return mmap(f(a), F.Constant1[B](a)) + }) +} + +func Chain[A, B, HKTA, HKTB any]( + mchain func(HKTA, func(A) HKTB) HKTB, + f func(A) HKTB, +) func(HKTA) HKTB { + return func(first HKTA) HKTB { + return MonadChain[A, B](mchain, first, f) + } +} diff --git a/internal/chain/testing/laws.go b/internal/chain/testing/laws.go new file mode 100644 index 0000000..9b93d62 --- /dev/null +++ b/internal/chain/testing/laws.go @@ -0,0 +1,84 @@ +package testing + +import ( + "testing" + + E "github.com/ibm/fp-go/eq" + F "github.com/ibm/fp-go/function" + L "github.com/ibm/fp-go/internal/apply/testing" + "github.com/stretchr/testify/assert" +) + +// Chain associativity law +// +// F.chain(F.chain(fa, afb), bfc) <-> F.chain(fa, a => F.chain(afb(a), bfc)) +func AssertAssociativity[HKTA, HKTB, HKTC, A, B, C any](t *testing.T, + eq E.Eq[HKTC], + + fofa func(A) HKTA, + fofb func(B) HKTB, + fofc func(C) HKTC, + + chainab func(HKTA, func(A) HKTB) HKTB, + chainac func(HKTA, func(A) HKTC) HKTC, + chainbc func(HKTB, func(B) HKTC) HKTC, + + ab func(A) B, + bc func(B) C, +) func(fa HKTA) bool { + return func(fa HKTA) bool { + + afb := F.Flow2(ab, fofb) + bfc := F.Flow2(bc, fofc) + + left := chainbc(chainab(fa, afb), bfc) + + right := chainac(fa, func(a A) HKTC { + return chainbc(afb(a), bfc) + }) + + return assert.True(t, eq.Equals(left, right), "Chain associativity") + } +} + +// AssertLaws asserts the apply laws `identity`, `composition`, `associative composition` and `associativity` +func AssertLaws[HKTA, HKTB, HKTC, HKTAB, HKTBC, HKTAC, HKTABAC, A, B, C any](t *testing.T, + eqa E.Eq[HKTA], + eqc E.Eq[HKTC], + + fofa func(A) HKTA, + fofb func(B) HKTB, + fofc func(C) HKTC, + + fofab func(func(A) B) HKTAB, + fofbc func(func(B) C) HKTBC, + + faa func(HKTA, func(A) A) HKTA, + fab func(HKTA, func(A) B) HKTB, + fac func(HKTA, func(A) C) HKTC, + fbc func(HKTB, func(B) C) HKTC, + + fmap func(HKTBC, func(func(B) C) func(func(A) B) func(A) C) HKTABAC, + + chainab func(HKTA, func(A) HKTB) HKTB, + chainac func(HKTA, func(A) HKTC) HKTC, + chainbc func(HKTB, func(B) HKTC) HKTC, + + fapab func(HKTAB, HKTA) HKTB, + fapbc func(HKTBC, HKTB) HKTC, + fapac func(HKTAC, HKTA) HKTC, + + fapabac func(HKTABAC, HKTAB) HKTAC, + + ab func(A) B, + bc func(B) C, +) func(fa HKTA) bool { + // apply laws + apply := L.AssertLaws(t, eqa, eqc, fofab, fofbc, faa, fab, fac, fbc, fmap, fapab, fapbc, fapac, fapabac, ab, bc) + // chain laws + associativity := AssertAssociativity(t, eqc, fofa, fofb, fofc, chainab, chainac, chainbc, ab, bc) + + return func(fa HKTA) bool { + return apply(fa) && associativity(fa) + } +} diff --git a/internal/exec/exec.go b/internal/exec/exec.go new file mode 100644 index 0000000..6eb0aa5 --- /dev/null +++ b/internal/exec/exec.go @@ -0,0 +1,31 @@ +package exec + +import ( + "bytes" + "context" + "fmt" + "os/exec" + + EX "github.com/ibm/fp-go/exec" + + T "github.com/ibm/fp-go/tuple" +) + +func Exec(ctx context.Context, name string, args []string, in []byte) (EX.CommandOutput, error) { + // command input + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stdin = bytes.NewReader(in) + // command result + var stdOut bytes.Buffer + var stdErr bytes.Buffer + + cmd.Stdout = &stdOut + cmd.Stderr = &stdErr + // execute the command + err := cmd.Run() + if err != nil { + err = fmt.Errorf("command execution of [%s][%s] failed, stdout [%s], stderr [%s], cause [%w]", name, args, stdOut.String(), stdErr.String(), err) + } + // return the outputs + return T.MakeTuple2(stdOut.Bytes(), stdErr.Bytes()), err +} diff --git a/internal/functor/testing/laws.go b/internal/functor/testing/laws.go new file mode 100644 index 0000000..1387584 --- /dev/null +++ b/internal/functor/testing/laws.go @@ -0,0 +1,57 @@ +package testing + +import ( + "testing" + + E "github.com/ibm/fp-go/eq" + F "github.com/ibm/fp-go/function" + "github.com/stretchr/testify/assert" +) + +// Functor identity law +// +// F.map(fa, a => a) <-> fa +func AssertIdentity[HKTA, A any](t *testing.T, eq E.Eq[HKTA], fmap func(HKTA, func(A) A) HKTA) func(fa HKTA) bool { + return func(fa HKTA) bool { + return assert.True(t, eq.Equals(fa, fmap(fa, F.Identity[A])), "Functor identity law") + } +} + +// Functor composition law +// +// F.map(fa, a => bc(ab(a))) <-> F.map(F.map(fa, ab), bc) +func AssertComposition[HKTA, HKTB, HKTC, A, B, C any]( + t *testing.T, + + eq E.Eq[HKTC], + + fab func(HKTA, func(A) B) HKTB, + fac func(HKTA, func(A) C) HKTC, + fbc func(HKTB, func(B) C) HKTC, + ab func(A) B, + bc func(B) C, +) func(fa HKTA) bool { + return func(fa HKTA) bool { + return assert.True(t, eq.Equals(fac(fa, F.Flow2(ab, bc)), fbc(fab(fa, ab), bc)), "Functor composition law") + } +} + +// AssertLaws asserts the functor laws `identity` and `composition` +func AssertLaws[HKTA, HKTB, HKTC, A, B, C any](t *testing.T, + eqa E.Eq[HKTA], + eqc E.Eq[HKTC], + + faa func(HKTA, func(A) A) HKTA, + fab func(HKTA, func(A) B) HKTB, + fac func(HKTA, func(A) C) HKTC, + fbc func(HKTB, func(B) C) HKTC, + ab func(A) B, + bc func(B) C, +) func(fa HKTA) bool { + identity := AssertIdentity(t, eqa, faa) + composition := AssertComposition(t, eqc, fab, fac, fbc, ab, bc) + + return func(fa HKTA) bool { + return identity(fa) && composition(fa) + } +} diff --git a/internal/monad/testing/laws.go b/internal/monad/testing/laws.go new file mode 100644 index 0000000..b5ecae7 --- /dev/null +++ b/internal/monad/testing/laws.go @@ -0,0 +1,107 @@ +package testing + +import ( + "testing" + + E "github.com/ibm/fp-go/eq" + LA "github.com/ibm/fp-go/internal/applicative/testing" + LC "github.com/ibm/fp-go/internal/chain/testing" + "github.com/stretchr/testify/assert" +) + +// Apply monad left identity law +// +// M.chain(M.of(a), f) <-> f(a) +func AssertLeftIdentity[HKTA, HKTB, A, B any](t *testing.T, + eq E.Eq[HKTB], + + fofa func(A) HKTA, + fofb func(B) HKTB, + + fchain func(HKTA, func(A) HKTB) HKTB, + + ab func(A) B, +) func(a A) bool { + return func(a A) bool { + + f := func(a A) HKTB { + return fofb(ab(a)) + } + + left := fchain(fofa(a), f) + right := f(a) + + return assert.True(t, eq.Equals(left, right), "Monad left identity") + } +} + +// Apply monad right identity law +// +// M.chain(fa, M.of) <-> fa +func AssertRightIdentity[HKTA, A any](t *testing.T, + eq E.Eq[HKTA], + + fofa func(A) HKTA, + + fchain func(HKTA, func(A) HKTA) HKTA, +) func(fa HKTA) bool { + return func(fa HKTA) bool { + + left := fchain(fa, fofa) + right := fa + + return assert.True(t, eq.Equals(left, right), "Monad right identity") + } +} + +// AssertLaws asserts the apply laws `identity`, `composition`, `associative composition`, 'applicative identity', 'homomorphism', 'interchange', `associativity`, `left identity`, `right identity` +func AssertLaws[HKTA, HKTB, HKTC, HKTAA, HKTAB, HKTBC, HKTAC, HKTABB, HKTABAC, A, B, C any](t *testing.T, + eqa E.Eq[HKTA], + eqb E.Eq[HKTB], + eqc E.Eq[HKTC], + + fofa func(A) HKTA, + fofb func(B) HKTB, + fofc func(C) HKTC, + + fofaa func(func(A) A) HKTAA, + fofab func(func(A) B) HKTAB, + fofbc func(func(B) C) HKTBC, + fofabb func(func(func(A) B) B) HKTABB, + + faa func(HKTA, func(A) A) HKTA, + fab func(HKTA, func(A) B) HKTB, + fac func(HKTA, func(A) C) HKTC, + fbc func(HKTB, func(B) C) HKTC, + + fmap func(HKTBC, func(func(B) C) func(func(A) B) func(A) C) HKTABAC, + + chainaa func(HKTA, func(A) HKTA) HKTA, + chainab func(HKTA, func(A) HKTB) HKTB, + chainac func(HKTA, func(A) HKTC) HKTC, + chainbc func(HKTB, func(B) HKTC) HKTC, + + fapaa func(HKTAA, HKTA) HKTA, + fapab func(HKTAB, HKTA) HKTB, + fapbc func(HKTBC, HKTB) HKTC, + fapac func(HKTAC, HKTA) HKTC, + + fapabb func(HKTABB, HKTAB) HKTB, + fapabac func(HKTABAC, HKTAB) HKTAC, + + ab func(A) B, + bc func(B) C, +) func(a A) bool { + // applicative laws + applicative := LA.AssertLaws(t, eqa, eqb, eqc, fofa, fofb, fofaa, fofab, fofbc, fofabb, faa, fab, fac, fbc, fmap, fapaa, fapab, fapbc, fapac, fapabb, fapabac, ab, bc) + // chain laws + chain := LC.AssertLaws(t, eqa, eqc, fofa, fofb, fofc, fofab, fofbc, faa, fab, fac, fbc, fmap, chainab, chainac, chainbc, fapab, fapbc, fapac, fapabac, ab, bc) + // monad laws + leftIdentity := AssertLeftIdentity(t, eqb, fofa, fofb, chainab, ab) + rightIdentity := AssertRightIdentity(t, eqa, fofa, chainaa) + + return func(a A) bool { + fa := fofa(a) + return applicative(a) && chain(fa) && leftIdentity(a) && rightIdentity(fa) + } +} diff --git a/internal/record/record.go b/internal/record/record.go new file mode 100644 index 0000000..3a91c5f --- /dev/null +++ b/internal/record/record.go @@ -0,0 +1,33 @@ +package record + +func Reduce[M ~map[K]V, K comparable, V, R any](r M, f func(R, V) R, initial R) R { + current := initial + for _, v := range r { + current = f(current, v) + } + return current +} + +func ReduceWithIndex[M ~map[K]V, K comparable, V, R any](r M, f func(K, R, V) R, initial R) R { + current := initial + for k, v := range r { + current = f(k, current, v) + } + return current +} + +func ReduceRef[M ~map[K]V, K comparable, V, R any](r M, f func(R, *V) R, initial R) R { + current := initial + for _, v := range r { + current = f(current, &v) // #nosec G601 + } + return current +} + +func ReduceRefWithIndex[M ~map[K]V, K comparable, V, R any](r M, f func(K, R, *V) R, initial R) R { + current := initial + for k, v := range r { + current = f(k, current, &v) // #nosec G601 + } + return current +} diff --git a/internal/record/traverse.go b/internal/record/traverse.go new file mode 100644 index 0000000..eadfc14 --- /dev/null +++ b/internal/record/traverse.go @@ -0,0 +1,93 @@ +package record + +import ( + F "github.com/ibm/fp-go/function" +) + +// createEmpty creates a new empty, read-write map +// this is different to Empty which creates a new read-only empty map +func createEmpty[N ~map[K]A, K comparable, A any]() N { + return make(N) +} + +// inserts the key/value pair into a read-write map for performance +// order of parameters is adjusted to be curryable +func addKey[N ~map[K]A, K comparable, A any](key K, m N, value A) N { + m[key] = value + return m +} + +/* +* +We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces + +HKTRB = HKT +HKTA = HKT +HKTB = HKT +HKTAB = HKT +*/ +func traverseWithIndex[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any]( + _of func(MB) HKTRB, + _map func(HKTRB, func(MB) func(B) MB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + ta MA, f func(K, A) HKTB) HKTRB { + // this function inserts a value into a map with a given key + cb := F.Curry3(addKey[MB, K, B]) + + return ReduceWithIndex(ta, func(k K, r HKTRB, a A) HKTRB { + return _ap( + _map(r, cb(k)), + f(k, a), + ) + }, _of(createEmpty[MB]())) +} + +func MonadTraverse[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any]( + _of func(MB) HKTRB, + _map func(HKTRB, func(MB) func(B) MB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + r MA, f func(A) HKTB) HKTRB { + return traverseWithIndex(_of, _map, _ap, r, F.Ignore1st[K](f)) +} + +func TraverseWithIndex[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any]( + _of func(MB) HKTRB, + _map func(HKTRB, func(MB) func(B) MB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + f func(K, A) HKTB) func(MA) HKTRB { + + return func(ma MA) HKTRB { + return traverseWithIndex(_of, _map, _ap, ma, f) + } +} + +// HKTA = HKT +// HKTB = HKT +// HKTAB = HKT +// HKTRB = HKT +func Traverse[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any]( + _of func(MB) HKTRB, + _map func(HKTRB, func(MB) func(B) MB) HKTAB, + _ap func(HKTAB, HKTB) HKTRB, + + f func(A) HKTB) func(MA) HKTRB { + + return func(ma MA) HKTRB { + return MonadTraverse(_of, _map, _ap, ma, f) + } +} + +// HKTA = HKT[A] +// HKTAA = HKT[func(A)MA] +// HKTRA = HKT[MA] +func Sequence[MA ~map[K]A, MKTA ~map[K]HKTA, K comparable, A, HKTA, HKTAA, HKTRA any]( + _of func(MA) HKTRA, + _map func(HKTRA, func(MA) func(A) MA) HKTAA, + _ap func(HKTAA, HKTA) HKTRA, + + ma MKTA) HKTRA { + return MonadTraverse(_of, _map, _ap, ma, F.Identity[HKTA]) +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..79a8046 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,28 @@ +package utils + +import ( + "errors" + "strings" +) + +var Upper = strings.ToUpper + +func Sum(left, right int) int { + return left + right +} + +func Double(value int) int { + return value * 2 +} + +func Triple(value int) int { + return value * 3 +} + +func StringLen(value string) int { + return len(value) +} + +func Error() (int, error) { + return 0, errors.New("some error") +} diff --git a/logging/logger.go b/logging/logger.go new file mode 100644 index 0000000..2f6d006 --- /dev/null +++ b/logging/logger.go @@ -0,0 +1,18 @@ +package Logging + +import ( + "log" +) + +func LoggingCallbacks(loggers ...*log.Logger) (func(string, ...any), func(string, ...any)) { + switch len(loggers) { + case 0: + def := log.Default() + return def.Printf, def.Printf + case 1: + log0 := loggers[0] + return log0.Printf, log0.Printf + default: + return loggers[0].Printf, loggers[1].Printf + } +} diff --git a/magma/array.go b/magma/array.go new file mode 100644 index 0000000..4e8929e --- /dev/null +++ b/magma/array.go @@ -0,0 +1,29 @@ +package magma + +import ( + F "github.com/ibm/fp-go/function" + AR "github.com/ibm/fp-go/internal/array" +) + +func GenericMonadConcatAll[GA ~[]A, A any](m Magma[A]) func(GA, A) A { + return func(as GA, first A) A { + return AR.Reduce(as, m.Concat, first) + } +} + +// GenericConcatAll concats all items using the semigroup and a starting value +func GenericConcatAll[GA ~[]A, A any](m Magma[A]) func(A) func(GA) A { + ca := GenericMonadConcatAll[GA](m) + return func(a A) func(GA) A { + return F.Bind2nd(ca, a) + } +} + +func MonadConcatAll[A any](m Magma[A]) func([]A, A) A { + return GenericMonadConcatAll[[]A](m) +} + +// ConcatAll concats all items using the semigroup and a starting value +func ConcatAll[A any](m Magma[A]) func(A) func([]A) A { + return GenericConcatAll[[]A](m) +} diff --git a/magma/magma.go b/magma/magma.go new file mode 100644 index 0000000..ce6e6b3 --- /dev/null +++ b/magma/magma.go @@ -0,0 +1,84 @@ +package magma + +type Magma[A any] interface { + Concat(x A, y A) A +} + +type magma[A any] struct { + c func(A, A) A +} + +func (self magma[A]) Concat(x A, y A) A { + return self.c(x, y) +} + +func MakeMagma[A any](c func(A, A) A) Magma[A] { + return magma[A]{c: c} +} + +func Reverse[A any](m Magma[A]) Magma[A] { + return MakeMagma(func(x A, y A) A { + return m.Concat(y, y) + }) +} + +func filterFirst[A any](p func(A) bool, c func(A, A) A, x A, y A) A { + if p(x) { + return c(x, y) + } + return y +} + +func filterSecond[A any](p func(A) bool, c func(A, A) A, x A, y A) A { + if p(y) { + return c(x, y) + } + return x +} + +func FilterFirst[A any](p func(A) bool) func(Magma[A]) Magma[A] { + return func(m Magma[A]) Magma[A] { + c := m.Concat + return MakeMagma(func(x A, y A) A { + return filterFirst(p, c, x, y) + }) + } +} + +func FilterSecond[A any](p func(A) bool) func(Magma[A]) Magma[A] { + return func(m Magma[A]) Magma[A] { + c := m.Concat + return MakeMagma(func(x A, y A) A { + return filterSecond(p, c, x, y) + }) + } +} + +func first[A any](x A, y A) A { + return x +} + +func second[A any](x A, y A) A { + return y +} + +func First[A any]() Magma[A] { + return MakeMagma(first[A]) +} + +func Second[A any]() Magma[A] { + return MakeMagma(second[A]) +} + +func endo[A any](f func(A) A, c func(A, A) A, x A, y A) A { + return c(f(x), f(y)) +} + +func Endo[A any](f func(A) A) func(Magma[A]) Magma[A] { + return func(m Magma[A]) Magma[A] { + c := m.Concat + return MakeMagma(func(x A, y A) A { + return endo(f, c, x, y) + }) + } +} diff --git a/magma/magma_test.go b/magma/magma_test.go new file mode 100644 index 0000000..1d179a6 --- /dev/null +++ b/magma/magma_test.go @@ -0,0 +1,19 @@ +package magma + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFirst(t *testing.T) { + m := First[string]() + + assert.Equal(t, "a", m.Concat("a", "b")) +} + +func TestSecond(t *testing.T) { + m := Second[string]() + + assert.Equal(t, "b", m.Concat("a", "b")) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..0875452 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + + O "github.com/ibm/fp-go/option" +) + +func isNonemptyString(val string) bool { + return val != "" +} + +// var O = OptionModule{of: O_of, some: O_of, none: none, mp: OMap} + +func main() { + + opt_string := O.FromPredicate(isNonemptyString) + + stringO1 := opt_string("Carsten") + stringO2 := opt_string("") + + fmt.Println(stringO1) + fmt.Println(stringO2) + +} diff --git a/monoid/apply.go b/monoid/apply.go new file mode 100644 index 0000000..1704bc5 --- /dev/null +++ b/monoid/apply.go @@ -0,0 +1,19 @@ +package monoid + +import ( + S "github.com/ibm/fp-go/semigroup" +) + +func ApplicativeMonoid[A, HKTA, HKTFA any]( + _of func(A) HKTA, + _map func(HKTA, func(A) func(A) A) HKTFA, + _ap func(HKTFA, HKTA) HKTA, + + m Monoid[A], +) Monoid[HKTA] { + + return MakeMonoid( + S.ApplySemigroup[A](_map, _ap, m).Concat, + _of(m.Empty()), + ) +} diff --git a/monoid/array.go b/monoid/array.go new file mode 100644 index 0000000..ade2e5d --- /dev/null +++ b/monoid/array.go @@ -0,0 +1,19 @@ +package monoid + +import ( + S "github.com/ibm/fp-go/semigroup" +) + +func GenericConcatAll[GA ~[]A, A any](m Monoid[A]) func(GA) A { + return S.GenericConcatAll[GA](S.MakeSemigroup(m.Concat))(m.Empty()) +} + +// ConcatAll concatenates all values using the monoid and the default empty value +func ConcatAll[A any](m Monoid[A]) func([]A) A { + return GenericConcatAll[[]A](m) +} + +// Fold concatenates all values using the monoid and the default empty value +func Fold[A any](m Monoid[A]) func([]A) A { + return GenericConcatAll[[]A](m) +} diff --git a/monoid/function.go b/monoid/function.go new file mode 100644 index 0000000..4c0b079 --- /dev/null +++ b/monoid/function.go @@ -0,0 +1,14 @@ +package monoid + +import ( + F "github.com/ibm/fp-go/function" + S "github.com/ibm/fp-go/semigroup" +) + +// FunctionMonoid forms a monoid as long as you can provide a monoid for the codomain. +func FunctionMonoid[A, B any](M Monoid[B]) Monoid[func(A) B] { + return MakeMonoid( + S.FunctionSemigroup[A, B](M).Concat, + F.Constant1[A](M.Empty()), + ) +} diff --git a/monoid/monoid.go b/monoid/monoid.go new file mode 100644 index 0000000..d39424f --- /dev/null +++ b/monoid/monoid.go @@ -0,0 +1,33 @@ +package monoid + +import ( + S "github.com/ibm/fp-go/semigroup" +) + +type Monoid[A any] interface { + S.Semigroup[A] + Empty() A +} + +type monoid[A any] struct { + c func(A, A) A + e A +} + +func (self monoid[A]) Concat(x A, y A) A { + return self.c(x, y) +} + +func (self monoid[A]) Empty() A { + return self.e +} + +// MakeMonoid creates a monoid given a concat function and an empty element +func MakeMonoid[A any](c func(A, A) A, e A) Monoid[A] { + return monoid[A]{c: c, e: e} +} + +// Reverse returns the dual of a `Monoid`, obtained by swapping the arguments of `Concat`. +func Reverse[A any](m Monoid[A]) Monoid[A] { + return MakeMonoid(S.Reverse[A](m).Concat, m.Empty()) +} diff --git a/monoid/testing/rules.go b/monoid/testing/rules.go new file mode 100644 index 0000000..ef5c1d7 --- /dev/null +++ b/monoid/testing/rules.go @@ -0,0 +1,28 @@ +package testing + +import ( + "testing" + + AR "github.com/ibm/fp-go/internal/array" + M "github.com/ibm/fp-go/monoid" + "github.com/stretchr/testify/assert" +) + +func assertLaws[A any](t *testing.T, m M.Monoid[A]) func(a A) bool { + e := m.Empty() + return func(a A) bool { + return assert.Equal(t, a, m.Concat(a, e), "Monoid right identity") && + assert.Equal(t, a, m.Concat(e, a), "Monoid left identity") + } +} + +// AssertLaws asserts the monoid laws for a dataset +func AssertLaws[A any](t *testing.T, m M.Monoid[A]) func(data []A) bool { + law := assertLaws(t, m) + + return func(data []A) bool { + return AR.Reduce(data, func(result bool, value A) bool { + return result && law(value) + }, true) + } +} diff --git a/number/magma.go b/number/magma.go new file mode 100644 index 0000000..2e8ba7a --- /dev/null +++ b/number/magma.go @@ -0,0 +1,11 @@ +package number + +import ( + M "github.com/ibm/fp-go/magma" +) + +func MagmaSub[A int | int8 | int16 | int32 | int64 | float32 | float64 | complex64 | complex128]() M.Magma[A] { + return M.MakeMagma(func(first A, second A) A { + return first - second + }) +} diff --git a/number/magma_test.go b/number/magma_test.go new file mode 100644 index 0000000..2f6e9ba --- /dev/null +++ b/number/magma_test.go @@ -0,0 +1,18 @@ +package number + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + M "github.com/ibm/fp-go/magma" +) + +func TestSemigroupIsMagma(t *testing.T) { + sum := SemigroupSum[int]() + + var magma M.Magma[int] = sum + + assert.Equal(t, sum.Concat(1, 2), magma.Concat(1, 2)) + assert.Equal(t, sum.Concat(1, 2), sum.Concat(2, 1)) +} diff --git a/number/monoid.go b/number/monoid.go new file mode 100644 index 0000000..c523a3e --- /dev/null +++ b/number/monoid.go @@ -0,0 +1,13 @@ +package number + +import ( + M "github.com/ibm/fp-go/monoid" +) + +func MonoidSum[A int | int8 | int16 | int32 | int64 | float32 | float64 | complex64 | complex128]() M.Monoid[A] { + s := SemigroupSum[A]() + return M.MakeMonoid( + s.Concat, + 0, + ) +} diff --git a/number/monoid_test.go b/number/monoid_test.go new file mode 100644 index 0000000..2d9c2ff --- /dev/null +++ b/number/monoid_test.go @@ -0,0 +1,11 @@ +package number + +import ( + "testing" + + M "github.com/ibm/fp-go/monoid/testing" +) + +func TestMonoidSum(t *testing.T) { + M.AssertLaws(t, MonoidSum[int]())([]int{0, 1, 1000, -1}) +} diff --git a/number/semigroup.go b/number/semigroup.go new file mode 100644 index 0000000..421f2b3 --- /dev/null +++ b/number/semigroup.go @@ -0,0 +1,11 @@ +package number + +import ( + S "github.com/ibm/fp-go/semigroup" +) + +func SemigroupSum[A int | int8 | int16 | int32 | int64 | float32 | float64 | complex64 | complex128]() S.Semigroup[A] { + return S.MakeSemigroup(func(first A, second A) A { + return first + second + }) +} diff --git a/option/apply.go b/option/apply.go new file mode 100644 index 0000000..fb8a1dd --- /dev/null +++ b/option/apply.go @@ -0,0 +1,14 @@ +package option + +import ( + M "github.com/ibm/fp-go/monoid" + S "github.com/ibm/fp-go/semigroup" +) + +func ApplySemigroup[A any](s S.Semigroup[A]) S.Semigroup[Option[A]] { + return S.ApplySemigroup(MonadMap[A, func(A) A], MonadAp[A, A], s) +} + +func ApplicativeMonoid[A any](m M.Monoid[A]) M.Monoid[Option[A]] { + return M.ApplicativeMonoid(Of[A], MonadMap[A, func(A) A], MonadAp[A, A], m) +} diff --git a/option/array.go b/option/array.go new file mode 100644 index 0000000..f2cba77 --- /dev/null +++ b/option/array.go @@ -0,0 +1,31 @@ +package option + +import ( + F "github.com/ibm/fp-go/function" + RA "github.com/ibm/fp-go/internal/array" +) + +// TraverseArray transforms an array +func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f func(A) Option[B]) func(GA) Option[GB] { + return RA.Traverse[GA]( + Of[GB], + MonadMap[GB, func(B) GB], + MonadAp[B, GB], + + f, + ) +} + +// TraverseArray transforms an array +func TraverseArray[A, B any](f func(A) Option[B]) func([]A) Option[[]B] { + return TraverseArrayG[[]A, []B](f) +} + +func SequenceArrayG[GA ~[]A, GOA ~[]Option[A], A any](ma GOA) Option[GA] { + return TraverseArrayG[GOA, GA](F.Identity[Option[A]])(ma) +} + +// SequenceArray converts a homogeneous sequence of either into an either of sequence +func SequenceArray[A any](ma []Option[A]) Option[[]A] { + return SequenceArrayG[[]A](ma) +} diff --git a/option/array_test.go b/option/array_test.go new file mode 100644 index 0000000..4ec5c31 --- /dev/null +++ b/option/array_test.go @@ -0,0 +1,21 @@ +package option + +import ( + "testing" + + F "github.com/ibm/fp-go/function" + "github.com/stretchr/testify/assert" +) + +func TestSequenceArray(t *testing.T) { + + one := Of(1) + two := Of(2) + + res := F.Pipe1( + []Option[int]{one, two}, + SequenceArray[int], + ) + + assert.Equal(t, res, Of([]int{1, 2})) +} diff --git a/option/eq.go b/option/eq.go new file mode 100644 index 0000000..50fcda3 --- /dev/null +++ b/option/eq.go @@ -0,0 +1,22 @@ +package option + +import ( + EQ "github.com/ibm/fp-go/eq" + F "github.com/ibm/fp-go/function" +) + +// Constructs an equal predicate for an `Option` +func Eq[A any](a EQ.Eq[A]) EQ.Eq[Option[A]] { + // some convenient shortcuts + fld := Fold( + F.Constant(Fold(F.ConstTrue, F.Constant1[A](false))), + F.Flow2(F.Curry2(a.Equals), F.Bind1st(Fold[A, bool], F.ConstFalse)), + ) + // convert to an equals predicate + return EQ.FromEquals(F.Uncurry2(fld)) +} + +// FromStrictEquals constructs an `Eq` from the canonical comparison function +func FromStrictEquals[A comparable]() EQ.Eq[Option[A]] { + return Eq(EQ.FromStrictEquals[A]()) +} diff --git a/option/eq_test.go b/option/eq_test.go new file mode 100644 index 0000000..5fd2e8e --- /dev/null +++ b/option/eq_test.go @@ -0,0 +1,26 @@ +package option + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEq(t *testing.T) { + + r1 := Of(1) + r2 := Of(1) + r3 := Of(2) + + n1 := None[int]() + + eq := FromStrictEquals[int]() + + assert.True(t, eq.Equals(r1, r1)) + assert.True(t, eq.Equals(r1, r2)) + assert.False(t, eq.Equals(r1, r3)) + assert.False(t, eq.Equals(r1, n1)) + + assert.True(t, eq.Equals(n1, n1)) + assert.False(t, eq.Equals(n1, r2)) +} diff --git a/option/legacy.go b/option/legacy.go new file mode 100644 index 0000000..7831de3 --- /dev/null +++ b/option/legacy.go @@ -0,0 +1,58 @@ +package option + +import "fmt" + +type OptionTag int + +const ( + NoneTag OptionTag = iota + SomeTag +) + +// Option defines a data structure that logically holds a value or not +type Option[A any] struct { + Tag OptionTag + Value A +} + +// String prints some debug info for the object +func (s Option[A]) String() string { + switch s.Tag { + case NoneTag: + return fmt.Sprintf("None[%T]", s.Value) + case SomeTag: + return fmt.Sprintf("Some[%T](%v)", s.Value, s.Value) + } + return "Invalid" +} + +func IsNone[T any](val Option[T]) bool { + return val.Tag == NoneTag +} + +func Some[T any](value T) Option[T] { + return Option[T]{Tag: SomeTag, Value: value} +} + +func Of[T any](value T) Option[T] { + return Some(value) +} + +func None[T any]() Option[T] { + return Option[T]{Tag: NoneTag} +} + +func IsSome[T any](val Option[T]) bool { + return val.Tag == SomeTag +} + +func MonadFold[A, B any](ma Option[A], onNone func() B, onSome func(A) B) B { + if IsNone(ma) { + return onNone() + } + return onSome(ma.Value) +} + +func Unwrap[A any](ma Option[A]) (A, bool) { + return ma.Value, ma.Tag == SomeTag +} diff --git a/option/logger.go b/option/logger.go new file mode 100644 index 0000000..a78abcf --- /dev/null +++ b/option/logger.go @@ -0,0 +1,33 @@ +package option + +import ( + "log" + + F "github.com/ibm/fp-go/function" + L "github.com/ibm/fp-go/logging" +) + +func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) func(Option[A]) Option[A] { + return Fold( + func() Option[A] { + left("%s", prefix) + return None[A]() + }, + func(a A) Option[A] { + right("%s: %v", prefix, a) + return Some(a) + }) +} + +func Logger[A any](loggers ...*log.Logger) func(string) func(Option[A]) Option[A] { + left, right := L.LoggingCallbacks(loggers...) + return func(prefix string) func(Option[A]) Option[A] { + delegate := _log[A](left, right, prefix) + return func(ma Option[A]) Option[A] { + return F.Pipe1( + delegate(ma), + ChainTo[A](ma), + ) + } + } +} diff --git a/option/modern._go b/option/modern._go new file mode 100644 index 0000000..0e5c71c --- /dev/null +++ b/option/modern._go @@ -0,0 +1,61 @@ +//go:build disabled + +package option + +// Option will be of type T or None +type Option[T any] interface { + fmt.Stringer +} + +type none struct { +} + +var const_none = none{} + +func (none) String() string { + return "None" +} + +type some[T any] struct { + v T +} + +func (s some[T]) String() string { + return fmt.Sprintf("Some[%v]", s.v) +} + +func IsNone[T any](val Option[T]) bool { + return val == const_none +} + +func Some[T any](value T) Option[T] { + return some[T]{v: value} +} + +func Of[T any](value T) Option[T] { + return Some(value) +} + +func None[T any]() Option[T] { + return const_none +} + +func IsSome[T any](val Option[T]) bool { + return !IsNone(val) +} + +func MonadFold[A, B any](ma Option[A], onNone func() B, onSome func(A) B) B { + if IsNone(ma) { + return onNone() + } + return onSome(ma.(some[A]).v) +} + +func Unwrap[A any](a A) func(Option[A]) (A, bool) { + return func(ma Option[A]) (A, bool) { + if IsNone(ma) { + return a, false + } + return ma.(some[A]).v, true + } +} diff --git a/option/monoid.go b/option/monoid.go new file mode 100644 index 0000000..541a718 --- /dev/null +++ b/option/monoid.go @@ -0,0 +1,38 @@ +package option + +import ( + F "github.com/ibm/fp-go/function" + M "github.com/ibm/fp-go/monoid" + S "github.com/ibm/fp-go/semigroup" +) + +func Semigroup[A any]() func(S.Semigroup[A]) S.Semigroup[Option[A]] { + return func(s S.Semigroup[A]) S.Semigroup[Option[A]] { + concat := s.Concat + return S.MakeSemigroup( + func(x, y Option[A]) Option[A] { + return MonadFold(x, F.Constant(y), func(left A) Option[A] { + return MonadFold(y, F.Constant(x), func(right A) Option[A] { + return Some(concat(left, right)) + }) + }) + }, + ) + } +} + +// Monoid returning the left-most non-`None` value. If both operands are `Some`s then the inner values are +// concatenated using the provided `Semigroup` +// +// | x | y | concat(x, y) | +// | ------- | ------- | ------------------ | +// | none | none | none | +// | some(a) | none | some(a) | +// | none | some(b) | some(b) | +// | some(a) | some(b) | some(concat(a, b)) | +func Monoid[A any]() func(S.Semigroup[A]) M.Monoid[Option[A]] { + sg := Semigroup[A]() + return func(s S.Semigroup[A]) M.Monoid[Option[A]] { + return M.MakeMonoid(sg(s).Concat, None[A]()) + } +} diff --git a/option/number/number.go b/option/number/number.go new file mode 100644 index 0000000..4060789 --- /dev/null +++ b/option/number/number.go @@ -0,0 +1,20 @@ +package number + +import ( + "strconv" + + F "github.com/ibm/fp-go/function" + O "github.com/ibm/fp-go/option" +) + +func atoi(value string) (int, bool) { + data, err := strconv.Atoi(value) + return data, err == nil +} + +var ( + // Atoi converts a string to an integer + Atoi = O.Optionize1(atoi) + // Itoa converts an integer to a string + Itoa = F.Flow2(strconv.Itoa, O.Of[string]) +) diff --git a/option/option.go b/option/option.go new file mode 100644 index 0000000..4269a39 --- /dev/null +++ b/option/option.go @@ -0,0 +1,205 @@ +// package option implements the Option monad, a data type that can have a defined value or none +package option + +import ( + F "github.com/ibm/fp-go/function" +) + +func fromPredicate[A any](a A, pred func(A) bool) Option[A] { + if pred(a) { + return Some(a) + } + return None[A]() +} + +func FromPredicate[A any](pred func(A) bool) func(A) Option[A] { + return F.Bind2nd(fromPredicate[A], pred) +} + +func FromNillable[A any](a *A) Option[*A] { + return fromPredicate(a, F.IsNonNil[A]) +} + +func fromValidation[A, B any](a A, f func(A) (B, bool)) Option[B] { + b, ok := f(a) + if ok { + return Some(b) + } + return None[B]() +} + +func fromValidation0[A any](f func() (A, bool)) Option[A] { + a, ok := f() + if ok { + return Some(a) + } + return None[A]() +} + +func FromValidation[A, B any](f func(A) (B, bool)) func(A) Option[B] { + return F.Bind2nd(fromValidation[A, B], f) +} + +// MonadAp is the applicative functor of Option +func MonadAp[A, B any](fab Option[func(A) B], fa Option[A]) Option[B] { + return MonadFold(fab, None[B], func(ab func(A) B) Option[B] { + return MonadFold(fa, None[B], F.Flow2(ab, Some[B])) + }) +} + +func Ap[A, B any](fa Option[A]) func(Option[func(A) B]) Option[B] { + return F.Bind2nd(MonadAp[A, B], fa) +} + +func MonadMap[A, B any](fa Option[A], f func(A) B) Option[B] { + return MonadChain(fa, F.Flow2(f, Some[B])) +} + +func Map[A, B any](f func(a A) B) func(Option[A]) Option[B] { + return Chain(F.Flow2(f, Some[B])) +} + +func MonadMapTo[A, B any](fa Option[A], b B) Option[B] { + return MonadMap(fa, F.Constant1[A](b)) +} + +func MapTo[A, B any](b B) func(Option[A]) Option[B] { + return F.Bind2nd(MonadMapTo[A, B], b) +} + +func TryCatch[A any](f func() (A, error)) Option[A] { + val, err := f() + if err != nil { + return None[A]() + } + return Some(val) +} + +func Fold[A, B any](onNone func() B, onSome func(a A) B) func(ma Option[A]) B { + return func(ma Option[A]) B { + return MonadFold(ma, onNone, onSome) + } +} + +func GetOrElse[A any](onNone func() A) func(Option[A]) A { + return Fold(onNone, F.Identity[A]) +} + +func MonadChain[A, B any](fa Option[A], f func(A) Option[B]) Option[B] { + return MonadFold(fa, None[B], f) +} + +func Chain[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] { + return F.Bind2nd(MonadChain[A, B], f) +} + +func MonadChainTo[A, B any](ma Option[A], mb Option[B]) Option[B] { + return mb +} + +func ChainTo[A, B any](mb Option[B]) func(Option[A]) Option[B] { + return F.Bind2nd(MonadChainTo[A, B], mb) +} + +func MonadChainFirst[A, B any](ma Option[A], f func(A) Option[B]) Option[A] { + return MonadChain(ma, func(a A) Option[A] { + return MonadMap(f(a), F.Constant1[B](a)) + }) +} + +func ChainFirst[A, B any](f func(A) Option[B]) func(Option[A]) Option[A] { + return F.Bind2nd(MonadChainFirst[A, B], f) +} + +func Flatten[A any](mma Option[Option[A]]) Option[A] { + return MonadChain(mma, F.Identity[Option[A]]) +} + +func Alt[A any](that func() Option[A]) func(Option[A]) Option[A] { + return Fold(that, Of[A]) +} + +func MonadSequence2[T1, T2, R any](o1 Option[T1], o2 Option[T2], f func(T1, T2) Option[R]) Option[R] { + return MonadFold(o1, None[R], func(t1 T1) Option[R] { + return MonadFold(o2, None[R], func(t2 T2) Option[R] { + return f(t1, t2) + }) + }) +} + +func Sequence2[T1, T2, R any](f func(T1, T2) Option[R]) func(Option[T1], Option[T2]) Option[R] { + return func(o1 Option[T1], o2 Option[T2]) Option[R] { + return MonadSequence2(o1, o2, f) + } +} + +func Reduce[A, B any](f func(B, A) B, initial B) func(Option[A]) B { + return Fold(F.Constant(initial), F.Bind1st(f, initial)) +} + +// Filter converts an optional onto itself if it is some and the predicate is true +func Filter[A any](pred func(A) bool) func(Option[A]) Option[A] { + return Fold(None[A], F.Ternary(pred, Of[A], F.Ignore1[A](None[A]))) +} + +func Optionize0[R any](f func() (R, bool)) func() Option[R] { + return func() Option[R] { + return fromValidation0(f) + } +} + +func Optionize1[T1, R any](f func(T1) (R, bool)) func(T1) Option[R] { + return func(t1 T1) Option[R] { + return fromValidation0(func() (R, bool) { + return f(t1) + }) + } +} + +func Unoptionize1[T1, R any](f func(T1) Option[R]) func(T1) (R, bool) { + return func(t1 T1) (R, bool) { + return Unwrap(f(t1)) + } +} + +func Optionize2[T1, T2, R any](f func(t1 T1, t2 T2) (R, bool)) func(t1 T1, t2 T2) Option[R] { + return func(t1 T1, t2 T2) Option[R] { + return fromValidation0(func() (R, bool) { + return f(t1, t2) + }) + } +} + +func Unoptionize2[T1, T2, R any](f func(T1, T2) Option[R]) func(T1, T2) (R, bool) { + return func(t1 T1, t2 T2) (R, bool) { + return Unwrap(f(t1, t2)) + } +} + +func Optionize3[T1, T2, T3, R any](f func(t1 T1, t2 T2, t3 T3) (R, bool)) func(t1 T1, t2 T2, t3 T3) Option[R] { + return func(t1 T1, t2 T2, t3 T3) Option[R] { + return fromValidation0(func() (R, bool) { + return f(t1, t2, t3) + }) + } +} + +func Unoptionize3[T1, T2, T3, R any](f func(T1, T2, T3) Option[R]) func(T1, T2, T3) (R, bool) { + return func(t1 T1, t2 T2, t3 T3) (R, bool) { + return Unwrap(f(t1, t2, t3)) + } +} + +func Optionize4[T1, T2, T3, T4, R any](f func(t1 T1, t2 T2, t3 T3, t4 T4) (R, bool)) func(t1 T1, t2 T2, t3 T3, t4 T4) Option[R] { + return func(t1 T1, t2 T2, t3 T3, t4 T4) Option[R] { + return fromValidation0(func() (R, bool) { + return f(t1, t2, t3, t4) + }) + } +} + +func Unoptionize4[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) Option[R]) func(T1, T2, T3, T4) (R, bool) { + return func(t1 T1, t2 T2, t3 T3, t4 T4) (R, bool) { + return Unwrap(f(t1, t2, t3, t4)) + } +} diff --git a/option/option_test.go b/option/option_test.go new file mode 100644 index 0000000..b1ec678 --- /dev/null +++ b/option/option_test.go @@ -0,0 +1,116 @@ +package option + +import ( + "fmt" + "testing" + + F "github.com/ibm/fp-go/function" + "github.com/ibm/fp-go/internal/utils" + "github.com/stretchr/testify/assert" +) + +func TestReduce(t *testing.T) { + + assert.Equal(t, 2, F.Pipe1(None[int](), Reduce(utils.Sum, 2))) + assert.Equal(t, 5, F.Pipe1(Some(3), Reduce(utils.Sum, 2))) +} + +func TestIsNone(t *testing.T) { + assert.True(t, IsNone(None[int]())) + assert.False(t, IsNone(Of(1))) +} + +func TestIsSome(t *testing.T) { + assert.True(t, IsSome(Of(1))) + assert.False(t, IsSome(None[int]())) +} + +func TestMapOption(t *testing.T) { + + assert.Equal(t, F.Pipe1(Some(2), Map(utils.Double)), Some(4)) + + assert.Equal(t, F.Pipe1(None[int](), Map(utils.Double)), None[int]()) +} + +func TestTryCachOption(t *testing.T) { + + res := TryCatch(utils.Error) + + assert.Equal(t, None[int](), res) +} + +func TestAp(t *testing.T) { + assert.Equal(t, Some(4), F.Pipe1( + Some(utils.Double), + Ap[int, int](Some(2)), + )) + + assert.Equal(t, None[int](), F.Pipe1( + Some(utils.Double), + Ap[int, int](None[int]()), + )) + + assert.Equal(t, None[int](), F.Pipe1( + None[func(int) int](), + Ap[int, int](Some(2)), + )) + + assert.Equal(t, None[int](), F.Pipe1( + None[func(int) int](), + Ap[int, int](None[int]()), + )) +} + +func TestChain(t *testing.T) { + f := func(n int) Option[int] { return Some(n * 2) } + g := func(_ int) Option[int] { return None[int]() } + + assert.Equal(t, Some(2), F.Pipe1( + Some(1), + Chain(f), + )) + + assert.Equal(t, None[int](), F.Pipe1( + None[int](), + Chain(f), + )) + + assert.Equal(t, None[int](), F.Pipe1( + Some(1), + Chain(g), + )) + + assert.Equal(t, None[int](), F.Pipe1( + None[int](), + Chain(g), + )) +} + +func TestFlatten(t *testing.T) { + assert.Equal(t, Of(1), F.Pipe1(Of(Of(1)), Flatten[int])) +} + +func TestFold(t *testing.T) { + f := F.Constant("none") + g := func(s string) string { return fmt.Sprintf("some%d", len(s)) } + + fold := Fold(f, g) + + assert.Equal(t, "none", fold(None[string]())) + assert.Equal(t, "some3", fold(Some("abc"))) +} + +func TestFromPredicate(t *testing.T) { + p := func(n int) bool { return n > 2 } + f := FromPredicate(p) + + assert.Equal(t, None[int](), f(1)) + assert.Equal(t, Some(3), f(3)) +} + +func TestAlt(t *testing.T) { + assert.Equal(t, Some(1), F.Pipe1(Some(1), Alt(F.Constant(Some(2))))) + assert.Equal(t, Some(2), F.Pipe1(Some(2), Alt(F.Constant(None[int]())))) + assert.Equal(t, Some(1), F.Pipe1(None[int](), Alt(F.Constant(Some(1))))) + assert.Equal(t, None[int](), F.Pipe1(None[int](), Alt(F.Constant(None[int]())))) +} diff --git a/option/record.go b/option/record.go new file mode 100644 index 0000000..e78aaf4 --- /dev/null +++ b/option/record.go @@ -0,0 +1,31 @@ +package option + +import ( + F "github.com/ibm/fp-go/function" + RR "github.com/ibm/fp-go/internal/record" +) + +// TraverseRecord transforms a record of options into an option of a record +func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f func(A) Option[B]) func(GA) Option[GB] { + return RR.Traverse[GA]( + Of[GB], + MonadMap[GB, func(B) GB], + MonadAp[B, GB], + + f, + ) +} + +// TraverseRecord transforms a record of options into an option of a record +func TraverseRecord[K comparable, A, B any](f func(A) Option[B]) func(map[K]A) Option[map[K]B] { + return TraverseRecordG[map[K]A, map[K]B](f) +} + +func SequenceRecordG[GA ~map[K]A, GOA ~map[K]Option[A], K comparable, A any](ma GOA) Option[GA] { + return TraverseRecordG[GOA, GA](F.Identity[Option[A]])(ma) +} + +// SequenceRecord converts a homogeneous sequence of either into an either of sequence +func SequenceRecord[K comparable, A any](ma map[K]Option[A]) Option[map[K]A] { + return SequenceRecordG[map[K]A](ma) +} diff --git a/option/record_test.go b/option/record_test.go new file mode 100644 index 0000000..2b0117d --- /dev/null +++ b/option/record_test.go @@ -0,0 +1,17 @@ +package option + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSequenceRecord(t *testing.T) { + assert.Equal(t, Of(map[string]string{ + "a": "A", + "b": "B", + }), SequenceRecord(map[string]Option[string]{ + "a": Of("A"), + "b": Of("B"), + })) +} diff --git a/option/sequence.go b/option/sequence.go new file mode 100644 index 0000000..2496a3b --- /dev/null +++ b/option/sequence.go @@ -0,0 +1,58 @@ +package option + +import ( + Apply "github.com/ibm/fp-go/apply" + F "github.com/ibm/fp-go/function" + T "github.com/ibm/fp-go/tuple" +) + +// HKTA = HKT +// HKTOA = HKT> +// +// Sequence converts an option of some higher kinded type into the higher kinded type of an option +func Sequence[A, HKTA, HKTOA any]( + _of func(Option[A]) HKTOA, + _map func(HKTA, func(A) Option[A]) HKTOA, +) func(Option[HKTA]) HKTOA { + return Fold(F.Nullary2(None[A], _of), F.Bind2nd(_map, Some[A])) +} + +// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple + +func SequenceT1[A any](a Option[A]) Option[T.Tuple1[A]] { + return Apply.SequenceT1( + MonadMap[A, T.Tuple1[A]], + + a, + ) +} + +func SequenceT2[A, B any](a Option[A], b Option[B]) Option[T.Tuple2[A, B]] { + return Apply.SequenceT2( + MonadMap[A, func(B) T.Tuple2[A, B]], + MonadAp[B, T.Tuple2[A, B]], + + a, b, + ) +} + +func SequenceT3[A, B, C any](a Option[A], b Option[B], c Option[C]) Option[T.Tuple3[A, B, C]] { + return Apply.SequenceT3( + MonadMap[A, func(B) func(C) T.Tuple3[A, B, C]], + MonadAp[B, func(C) T.Tuple3[A, B, C]], + MonadAp[C, T.Tuple3[A, B, C]], + + a, b, c, + ) +} + +func SequenceT4[A, B, C, D any](a Option[A], b Option[B], c Option[C], d Option[D]) Option[T.Tuple4[A, B, C, D]] { + return Apply.SequenceT4( + MonadMap[A, func(B) func(C) func(D) T.Tuple4[A, B, C, D]], + MonadAp[B, func(C) func(D) T.Tuple4[A, B, C, D]], + MonadAp[C, func(D) T.Tuple4[A, B, C, D]], + MonadAp[D, T.Tuple4[A, B, C, D]], + + a, b, c, d, + ) +} diff --git a/option/sequence_test.go b/option/sequence_test.go new file mode 100644 index 0000000..ba7840d --- /dev/null +++ b/option/sequence_test.go @@ -0,0 +1,29 @@ +package option + +import ( + "testing" + + T "github.com/ibm/fp-go/tuple" + "github.com/stretchr/testify/assert" +) + +func TestSequenceT(t *testing.T) { + // one argumemt + s1 := SequenceT1[int] + assert.Equal(t, Of(T.MakeTuple1(1)), s1(Of(1))) + + // two arguments + s2 := SequenceT2[int, string] + assert.Equal(t, Of(T.MakeTuple2(1, "a")), s2(Of(1), Of("a"))) + + // three arguments + s3 := SequenceT3[int, string, bool] + assert.Equal(t, Of(T.MakeTuple3(1, "a", true)), s3(Of(1), Of("a"), Of(true))) + + // four arguments + s4 := SequenceT4[int, string, bool, int] + assert.Equal(t, Of(T.MakeTuple4(1, "a", true, 2)), s4(Of(1), Of("a"), Of(true), Of(2))) + + // three with one none + assert.Equal(t, None[T.Tuple3[int, string, bool]](), s3(Of(1), Of("a"), None[bool]())) +} diff --git a/option/testing/laws.go b/option/testing/laws.go new file mode 100644 index 0000000..c74e979 --- /dev/null +++ b/option/testing/laws.go @@ -0,0 +1,59 @@ +package testing + +import ( + "testing" + + EQ "github.com/ibm/fp-go/eq" + L "github.com/ibm/fp-go/internal/monad/testing" + O "github.com/ibm/fp-go/option" +) + +// AssertLaws asserts the apply monad laws for the `Either` monad +func AssertLaws[A, B, C any](t *testing.T, + eqa EQ.Eq[A], + eqb EQ.Eq[B], + eqc EQ.Eq[C], + + ab func(A) B, + bc func(B) C, +) func(a A) bool { + + return L.AssertLaws(t, + O.Eq(eqa), + O.Eq(eqb), + O.Eq(eqc), + + O.Of[A], + O.Of[B], + O.Of[C], + + O.Of[func(A) A], + O.Of[func(A) B], + O.Of[func(B) C], + O.Of[func(func(A) B) B], + + O.MonadMap[A, A], + O.MonadMap[A, B], + O.MonadMap[A, C], + O.MonadMap[B, C], + + O.MonadMap[func(B) C, func(func(A) B) func(A) C], + + O.MonadChain[A, A], + O.MonadChain[A, B], + O.MonadChain[A, C], + O.MonadChain[B, C], + + O.MonadAp[A, A], + O.MonadAp[A, B], + O.MonadAp[B, C], + O.MonadAp[A, C], + + O.MonadAp[func(A) B, B], + O.MonadAp[func(A) B, func(A) C], + + ab, + bc, + ) + +} diff --git a/option/testing/laws_test.go b/option/testing/laws_test.go new file mode 100644 index 0000000..55d5288 --- /dev/null +++ b/option/testing/laws_test.go @@ -0,0 +1,32 @@ +package testing + +import ( + "fmt" + "testing" + + EQ "github.com/ibm/fp-go/eq" + "github.com/stretchr/testify/assert" +) + +func TestMonadLaws(t *testing.T) { + // some comparison + eqa := EQ.FromStrictEquals[bool]() + eqb := EQ.FromStrictEquals[int]() + eqc := EQ.FromStrictEquals[string]() + + ab := func(a bool) int { + if a { + return 1 + } + return 0 + } + + bc := func(b int) string { + return fmt.Sprintf("value %d", b) + } + + laws := AssertLaws(t, eqa, eqb, eqc, ab, bc) + + assert.True(t, laws(true)) + assert.True(t, laws(false)) +} diff --git a/option/type.go b/option/type.go new file mode 100644 index 0000000..9bc1124 --- /dev/null +++ b/option/type.go @@ -0,0 +1,14 @@ +package option + +func toType[T any](a any) (T, bool) { + b, ok := a.(T) + return b, ok +} + +func ToType[T any](src any) Option[T] { + return fromValidation(src, toType[T]) +} + +func ToAny[T any](src T) Option[any] { + return Of(any(src)) +} diff --git a/option/type_test.go b/option/type_test.go new file mode 100644 index 0000000..975e07b --- /dev/null +++ b/option/type_test.go @@ -0,0 +1,22 @@ +package option + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTypeConversion(t *testing.T) { + + var src any = "Carsten" + + dst := ToType[string](src) + assert.Equal(t, Some("Carsten"), dst) +} + +func TestInvalidConversion(t *testing.T) { + var src any = make(map[string]string) + + dst := ToType[int](src) + assert.Equal(t, None[int](), dst) +} diff --git a/ord/monoid.go b/ord/monoid.go new file mode 100644 index 0000000..7e87603 --- /dev/null +++ b/ord/monoid.go @@ -0,0 +1,37 @@ +package ord + +import ( + F "github.com/ibm/fp-go/function" + M "github.com/ibm/fp-go/monoid" + S "github.com/ibm/fp-go/semigroup" +) + +// Semigroup implements a two level ordering +func Semigroup[A any]() S.Semigroup[Ord[A]] { + return S.MakeSemigroup(func(first, second Ord[A]) Ord[A] { + return FromCompare(func(a, b A) int { + ox := first.Compare(a, b) + if ox != 0 { + return ox + } + return second.Compare(a, b) + }) + }) +} + +// Monoid implements a two level ordering such that +// - its `Concat(ord1, ord2)` operation will order first by `ord1`, and then by `ord2` +// - its `Empty` value is an `Ord` that always considers compared elements equal +func Monoid[A any]() M.Monoid[Ord[A]] { + return M.MakeMonoid(Semigroup[A]().Concat, FromCompare(F.Constant2[A, A](0))) +} + +// MaxSemigroup returns a semigroup where `concat` will return the maximum, based on the provided order. +func MaxSemigroup[A any](O Ord[A]) S.Semigroup[A] { + return S.MakeSemigroup(Max(O)) +} + +// MaxSemigroup returns a semigroup where `concat` will return the minimum, based on the provided order. +func MinSemigroup[A any](O Ord[A]) S.Semigroup[A] { + return S.MakeSemigroup(Min(O)) +} diff --git a/ord/ord.go b/ord/ord.go new file mode 100644 index 0000000..496aaaf --- /dev/null +++ b/ord/ord.go @@ -0,0 +1,166 @@ +package ord + +import ( + C "github.com/ibm/fp-go/constraints" + E "github.com/ibm/fp-go/eq" + F "github.com/ibm/fp-go/function" + P "github.com/ibm/fp-go/predicate" +) + +type Ord[T any] interface { + E.Eq[T] + Compare(x, y T) int +} + +type ord[T any] struct { + c func(x, y T) int + e func(x, y T) bool +} + +func (self ord[T]) Equals(x, y T) bool { + return self.e(x, y) +} + +func (self ord[T]) Compare(x, y T) int { + return self.c(x, y) +} + +// MakeOrd creates an instance of an Ord +func MakeOrd[T any](c func(x, y T) int, e func(x, y T) bool) Ord[T] { + return ord[T]{c: c, e: e} +} + +// MakeOrd creates an instance of an Ord from a compare function +func FromCompare[T any](compare func(T, T) int) Ord[T] { + return MakeOrd(compare, func(x, y T) bool { + return compare(x, y) == 0 + }) +} + +// Reverse creates an inverted ordering +func Reverse[T any](o Ord[T]) Ord[T] { + return MakeOrd(func(y, x T) int { + return o.Compare(x, y) + }, o.Equals) +} + +// Contramap creates an odering under a transformation function +func Contramap[A, B any](f func(B) A) func(Ord[A]) Ord[B] { + return func(o Ord[A]) Ord[B] { + return MakeOrd(func(x, y B) int { + return o.Compare(f(x), f(y)) + }, func(x, y B) bool { + return o.Equals(f(x), f(y)) + }) + } +} + +// Min takes the minimum of two values. If they are considered equal, the first argument is chosen +func Min[A any](o Ord[A]) func(A, A) A { + return func(a, b A) A { + if o.Compare(a, b) < 1 { + return a + } + return b + } +} + +// Max takes the maximum of two values. If they are considered equal, the first argument is chosen +func Max[A any](o Ord[A]) func(A, A) A { + return func(a, b A) A { + if o.Compare(a, b) >= 0 { + return a + } + return b + } +} + +// Clamp clamps a value between a minimum and a maximum +func Clamp[A any](o Ord[A]) func(A, A) func(A) A { + return func(low, hi A) func(A) A { + clow := F.Bind2nd(o.Compare, low) + chi := F.Bind2nd(o.Compare, hi) + return func(a A) A { + if clow(a) <= 0 { + return low + } + if chi(a) >= 0 { + return hi + } + return a + } + } +} + +func strictCompare[A C.Ordered](a, b A) int { + if a < b { + return -1 + } else if a > b { + return +1 + } else { + return 0 + } +} + +func strictEq[A comparable](a, b A) bool { + return a == b +} + +// FromStrictCompare implements the ordering based on the built in native order +func FromStrictCompare[A C.Ordered]() Ord[A] { + return MakeOrd(strictCompare[A], strictEq[A]) +} + +/** + * Test whether one value is _strictly less than_ another + */ +func Lt[A any](O Ord[A]) func(A) func(A) bool { + return func(second A) func(A) bool { + return func(first A) bool { + return O.Compare(first, second) < 0 + } + } +} + +/** + * Test whether one value is less or equal than_ another + */ +func Leq[A any](O Ord[A]) func(A) func(A) bool { + return func(second A) func(A) bool { + return func(first A) bool { + return O.Compare(first, second) <= 0 + } + } +} + +/** + * Test whether one value is _strictly greater than_ another + */ +func Gt[A any](O Ord[A]) func(A) func(A) bool { + return func(second A) func(A) bool { + return func(first A) bool { + return O.Compare(first, second) > 0 + } + } +} + +/** + * Test whether one value is greater or equal than_ another + */ +func Geq[A any](O Ord[A]) func(A) func(A) bool { + return func(second A) func(A) bool { + return func(first A) bool { + return O.Compare(first, second) >= 0 + } + } +} + +// Test whether a value is between a minimum (inclusive) and a maximum (exclusive) +func Between[A any](O Ord[A]) func(A, A) func(A) bool { + lt := Lt(O) + geq := Geq(O) + return func(lo, hi A) func(A) bool { + // returns the predicate + return P.And(lt(hi))(geq(lo)) + } +} diff --git a/predicate/bool.go b/predicate/bool.go new file mode 100644 index 0000000..2c62556 --- /dev/null +++ b/predicate/bool.go @@ -0,0 +1,25 @@ +package predicate + +func Not[A any](predicate func(A) bool) func(A) bool { + return func(a A) bool { + return !predicate((a)) + } +} + +// And creates a predicate that combines other predicates via && +func And[A any](second func(A) bool) func(func(A) bool) func(A) bool { + return func(first func(A) bool) func(A) bool { + return func(a A) bool { + return first(a) && second(a) + } + } +} + +// Or creates a predicate that combines other predicates via || +func Or[A any](second func(A) bool) func(func(A) bool) func(A) bool { + return func(first func(A) bool) func(A) bool { + return func(a A) bool { + return first(a) || second(a) + } + } +} diff --git a/predicate/contramap.go b/predicate/contramap.go new file mode 100644 index 0000000..2ed5cd5 --- /dev/null +++ b/predicate/contramap.go @@ -0,0 +1,15 @@ +package predicate + +import ( + F "github.com/ibm/fp-go/function" +) + +// ContraMap creates a predicate from an existing predicate given a mapping function +func ContraMap[A, B any](f func(B) A) func(func(A) bool) func(B) bool { + return func(pred func(A) bool) func(B) bool { + return F.Flow2( + f, + pred, + ) + } +} diff --git a/predicate/monoid.go b/predicate/monoid.go new file mode 100644 index 0000000..968a16d --- /dev/null +++ b/predicate/monoid.go @@ -0,0 +1,43 @@ +package predicate + +import ( + F "github.com/ibm/fp-go/function" + M "github.com/ibm/fp-go/monoid" + S "github.com/ibm/fp-go/semigroup" +) + +// SemigroupAny combines predicates via || +func SemigroupAny[A any](predicate func(A) bool) S.Semigroup[func(A) bool] { + return S.MakeSemigroup(func(first func(A) bool, second func(A) bool) func(A) bool { + return F.Pipe1( + first, + Or(second), + ) + }) +} + +// SemigroupAll combines predicates via && +func SemigroupAll[A any](predicate func(A) bool) S.Semigroup[func(A) bool] { + return S.MakeSemigroup(func(first func(A) bool, second func(A) bool) func(A) bool { + return F.Pipe1( + first, + And(second), + ) + }) +} + +// MonoidAny combines predicates via || +func MonoidAny[A any](predicate func(A) bool) S.Semigroup[func(A) bool] { + return M.MakeMonoid( + SemigroupAny(predicate).Concat, + F.Constant1[A](false), + ) +} + +// MonoidAll combines predicates via && +func MonoidAll[A any](predicate func(A) bool) S.Semigroup[func(A) bool] { + return M.MakeMonoid( + SemigroupAll(predicate).Concat, + F.Constant1[A](true), + ) +} diff --git a/resources/images/logo.png b/resources/images/logo.png new file mode 100644 index 0000000..94f5f2d Binary files /dev/null and b/resources/images/logo.png differ diff --git a/semigroup/apply.go b/semigroup/apply.go new file mode 100644 index 0000000..b7b0ae1 --- /dev/null +++ b/semigroup/apply.go @@ -0,0 +1,23 @@ +package semigroup + +import ( + F "github.com/ibm/fp-go/function" +) + +/* +* +HKTA = HKT +HKTFA = HKT +*/ +func ApplySemigroup[A, HKTA, HKTFA any]( + _map func(HKTA, func(A) func(A) A) HKTFA, + _ap func(HKTFA, HKTA) HKTA, + + s Semigroup[A], +) Semigroup[HKTA] { + + cb := F.Curry2(s.Concat) + return MakeSemigroup(func(first HKTA, second HKTA) HKTA { + return _ap(_map(first, cb), second) + }) +} diff --git a/semigroup/array.go b/semigroup/array.go new file mode 100644 index 0000000..361820f --- /dev/null +++ b/semigroup/array.go @@ -0,0 +1,21 @@ +package semigroup + +import ( + M "github.com/ibm/fp-go/magma" +) + +func GenericMonadConcatAll[GA ~[]A, A any](s Semigroup[A]) func(GA, A) A { + return M.GenericMonadConcatAll[GA](M.MakeMagma(s.Concat)) +} + +func GenericConcatAll[GA ~[]A, A any](s Semigroup[A]) func(A) func(GA) A { + return M.GenericConcatAll[GA](M.MakeMagma(s.Concat)) +} + +func MonadConcatAll[A any](s Semigroup[A]) func([]A, A) A { + return GenericMonadConcatAll[[]A](s) +} + +func ConcatAll[A any](s Semigroup[A]) func(A) func([]A) A { + return GenericConcatAll[[]A](s) +} diff --git a/semigroup/ord/semigroup.go b/semigroup/ord/semigroup.go new file mode 100644 index 0000000..130a84b --- /dev/null +++ b/semigroup/ord/semigroup.go @@ -0,0 +1,16 @@ +package ord + +import ( + O "github.com/ibm/fp-go/ord" + S "github.com/ibm/fp-go/semigroup" +) + +// Max gets a semigroup where `concat` will return the maximum, based on the provided order. +func Max[A any](o O.Ord[A]) S.Semigroup[A] { + return S.MakeSemigroup(O.Max(o)) +} + +// Min gets a semigroup where `concat` will return the minimum, based on the provided order. +func Min[A any](o O.Ord[A]) S.Semigroup[A] { + return S.MakeSemigroup(O.Min(o)) +} diff --git a/semigroup/semigroup.go b/semigroup/semigroup.go new file mode 100644 index 0000000..9b6d645 --- /dev/null +++ b/semigroup/semigroup.go @@ -0,0 +1,46 @@ +package semigroup + +import ( + F "github.com/ibm/fp-go/function" + M "github.com/ibm/fp-go/magma" +) + +type Semigroup[A any] interface { + M.Magma[A] +} + +type semigroup[A any] struct { + c func(A, A) A +} + +func (self semigroup[A]) Concat(x A, y A) A { + return self.c(x, y) +} + +func MakeSemigroup[A any](c func(A, A) A) Semigroup[A] { + return semigroup[A]{c: c} +} + +// Reverse returns The dual of a `Semigroup`, obtained by swapping the arguments of `concat`. +func Reverse[A any](m Semigroup[A]) Semigroup[A] { + return MakeSemigroup(M.Reverse[A](m).Concat) +} + +// FunctionSemigroup forms a semigroup as long as you can provide a semigroup for the codomain. +func FunctionSemigroup[A, B any](S Semigroup[B]) Semigroup[func(A) B] { + return MakeSemigroup(func(f func(A) B, g func(A) B) func(A) B { + return func(a A) B { + return S.Concat(f(a), g(a)) + } + }) +} + +// First always returns the first argument. +func First[A any]() Semigroup[A] { + return MakeSemigroup(F.First[A, A]) +} + +// Last always returns the last argument. +func Last[A any]() Semigroup[A] { + return MakeSemigroup(F.Second[A, A]) +} diff --git a/semigroup/semigroup_test.go b/semigroup/semigroup_test.go new file mode 100644 index 0000000..60f853b --- /dev/null +++ b/semigroup/semigroup_test.go @@ -0,0 +1,21 @@ +package semigroup + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFirst(t *testing.T) { + + first := First[int]() + + assert.Equal(t, 1, first.Concat(1, 2)) +} + +func TestLast(t *testing.T) { + + last := Last[int]() + + assert.Equal(t, 2, last.Concat(1, 2)) +} diff --git a/string/monoid.go b/string/monoid.go new file mode 100644 index 0000000..a541fb4 --- /dev/null +++ b/string/monoid.go @@ -0,0 +1,8 @@ +package string + +import ( + M "github.com/ibm/fp-go/monoid" +) + +// Monoid is the monoid implementing string concatenation +var Monoid = M.MakeMonoid(concat, "") diff --git a/string/monoid_test.go b/string/monoid_test.go new file mode 100644 index 0000000..380eb96 --- /dev/null +++ b/string/monoid_test.go @@ -0,0 +1,11 @@ +package string + +import ( + "testing" + + M "github.com/ibm/fp-go/monoid/testing" +) + +func TestMonoid(t *testing.T) { + M.AssertLaws(t, Monoid)([]string{"", "a", "some value"}) +} diff --git a/string/semigroup.go b/string/semigroup.go new file mode 100644 index 0000000..cbbc60c --- /dev/null +++ b/string/semigroup.go @@ -0,0 +1,15 @@ +package string + +import ( + "fmt" + + S "github.com/ibm/fp-go/semigroup" +) + +func concat(left string, right string) string { + return fmt.Sprintf("%s%s", left, right) +} + +func Semigroup() S.Semigroup[string] { + return S.MakeSemigroup(concat) +} diff --git a/string/string.go b/string/string.go new file mode 100644 index 0000000..683e434 --- /dev/null +++ b/string/string.go @@ -0,0 +1,49 @@ +package string + +import ( + "strings" + + F "github.com/ibm/fp-go/function" + O "github.com/ibm/fp-go/ord" +) + +var ( + // ToUpperCase converts the string to uppercase + ToUpperCase = strings.ToUpper + + // ToLowerCase converts the string to lowercase + ToLowerCase = strings.ToLower + + // Ord implements the default ordering for strings + Ord = O.FromStrictCompare[string]() +) + +func Eq(left string, right string) bool { + return left == right +} + +func ToBytes(s string) []byte { + return []byte(s) +} + +func IsEmpty(s string) bool { + return len(s) == 0 +} + +func IsNonEmpty(s string) bool { + return len(s) > 0 +} + +func Size(s string) int { + return len(s) +} + +// Includes returns a predicate that tests for the existence of the search string +func Includes(searchString string) func(string) bool { + return F.Bind2nd(strings.Contains, searchString) +} + +// Equals returns a predicate that tests if a string is equal +func Equals(other string) func(string) bool { + return F.Bind2nd(Eq, other) +} diff --git a/string/string_test.go b/string/string_test.go new file mode 100644 index 0000000..151c5f1 --- /dev/null +++ b/string/string_test.go @@ -0,0 +1,12 @@ +package string + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEmpty(t *testing.T) { + assert.True(t, IsEmpty("")) + assert.False(t, IsEmpty("Carsten")) +} diff --git a/tuple/monoid.go b/tuple/monoid.go new file mode 100644 index 0000000..c815dbf --- /dev/null +++ b/tuple/monoid.go @@ -0,0 +1,33 @@ +package tuple + +import ( + M "github.com/ibm/fp-go/monoid" +) + +// Monoid1 implements a monoid for a 1-tuple +func Monoid1[T1 any](m1 M.Monoid[T1]) M.Monoid[Tuple1[T1]] { + return M.MakeMonoid(func(l, r Tuple1[T1]) Tuple1[T1] { + return MakeTuple1(m1.Concat(l.F1, l.F1)) + }, MakeTuple1(m1.Empty())) +} + +// Monoid2 implements a monoid for a 2-tuple +func Monoid2[T1, T2 any](m1 M.Monoid[T1], m2 M.Monoid[T2]) M.Monoid[Tuple2[T1, T2]] { + return M.MakeMonoid(func(l, r Tuple2[T1, T2]) Tuple2[T1, T2] { + return MakeTuple2(m1.Concat(l.F1, l.F1), m2.Concat(l.F2, l.F2)) + }, MakeTuple2(m1.Empty(), m2.Empty())) +} + +// Monoid3 implements a monoid for a 3-tuple +func Monoid3[T1, T2, T3 any](m1 M.Monoid[T1], m2 M.Monoid[T2], m3 M.Monoid[T3]) M.Monoid[Tuple3[T1, T2, T3]] { + return M.MakeMonoid(func(l, r Tuple3[T1, T2, T3]) Tuple3[T1, T2, T3] { + return MakeTuple3(m1.Concat(l.F1, l.F1), m2.Concat(l.F2, l.F2), m3.Concat(l.F3, l.F3)) + }, MakeTuple3(m1.Empty(), m2.Empty(), m3.Empty())) +} + +// Monoid4 implements a monoid for a 4-tuple +func Monoid4[T1, T2, T3, T4 any](m1 M.Monoid[T1], m2 M.Monoid[T2], m3 M.Monoid[T3], m4 M.Monoid[T4]) M.Monoid[Tuple4[T1, T2, T3, T4]] { + return M.MakeMonoid(func(l, r Tuple4[T1, T2, T3, T4]) Tuple4[T1, T2, T3, T4] { + return MakeTuple4(m1.Concat(l.F1, l.F1), m2.Concat(l.F2, l.F2), m3.Concat(l.F3, l.F3), m4.Concat(l.F4, l.F4)) + }, MakeTuple4(m1.Empty(), m2.Empty(), m3.Empty(), m4.Empty())) +} diff --git a/tuple/ord.go b/tuple/ord.go new file mode 100644 index 0000000..c6dd671 --- /dev/null +++ b/tuple/ord.go @@ -0,0 +1,68 @@ +package tuple + +import ( + O "github.com/ibm/fp-go/ord" +) + +// Ord1 implements ordering on a 1-tuple +func Ord1[T1 any](o1 O.Ord[T1]) O.Ord[Tuple1[T1]] { + return O.MakeOrd(func(l, r Tuple1[T1]) int { + return o1.Compare(l.F1, r.F1) + }, func(l, r Tuple1[T1]) bool { + return o1.Equals(l.F1, r.F1) + }) +} + +// Ord2 implements ordering on a 2-tuple +func Ord2[T1, T2 any](o1 O.Ord[T1], o2 O.Ord[T2]) O.Ord[Tuple2[T1, T2]] { + return O.MakeOrd(func(l, r Tuple2[T1, T2]) int { + c := o1.Compare(l.F1, r.F1) + if c != 0 { + return c + } + c = o2.Compare(l.F2, r.F2) + return c + }, func(l, r Tuple2[T1, T2]) bool { + return o1.Equals(l.F1, r.F1) && o2.Equals(l.F2, r.F2) + }) +} + +// Ord3 implements ordering on a 3-tuple +func Ord3[T1, T2, T3 any](o1 O.Ord[T1], o2 O.Ord[T2], o3 O.Ord[T3]) O.Ord[Tuple3[T1, T2, T3]] { + return O.MakeOrd(func(l, r Tuple3[T1, T2, T3]) int { + c := o1.Compare(l.F1, r.F1) + if c != 0 { + return c + } + c = o2.Compare(l.F2, r.F2) + if c != 0 { + return c + } + c = o3.Compare(l.F3, r.F3) + return c + }, func(l, r Tuple3[T1, T2, T3]) bool { + return o1.Equals(l.F1, r.F1) && o2.Equals(l.F2, r.F2) && o3.Equals(l.F3, r.F3) + }) +} + +// Ord4 implements ordering on a 4-tuple +func Ord4[T1, T2, T3, T4 any](o1 O.Ord[T1], o2 O.Ord[T2], o3 O.Ord[T3], o4 O.Ord[T4]) O.Ord[Tuple4[T1, T2, T3, T4]] { + return O.MakeOrd(func(l, r Tuple4[T1, T2, T3, T4]) int { + c := o1.Compare(l.F1, r.F1) + if c != 0 { + return c + } + c = o2.Compare(l.F2, r.F2) + if c != 0 { + return c + } + c = o3.Compare(l.F3, r.F3) + if c != 0 { + return c + } + c = o4.Compare(l.F4, r.F4) + return c + }, func(l, r Tuple4[T1, T2, T3, T4]) bool { + return o1.Equals(l.F1, r.F1) && o2.Equals(l.F2, r.F2) && o3.Equals(l.F3, r.F3) && o4.Equals(l.F4, r.F4) + }) +} diff --git a/tuple/tuple.go b/tuple/tuple.go new file mode 100644 index 0000000..4e6a4a7 --- /dev/null +++ b/tuple/tuple.go @@ -0,0 +1,97 @@ +// Package tuple contains type definitions and functions for data structures for tuples of heterogenous types. For homogeneous types +// consider to use arrays for simplicity +package tuple + +// Tuple1 is a structure carrying one element +type Tuple1[T1 any] struct { + F1 T1 +} + +// Tuple2 is a structure carrying two elements +type Tuple2[T1, T2 any] struct { + F1 T1 + F2 T2 +} + +// Tuple3 is a structure carrying three elements +type Tuple3[T1, T2, T3 any] struct { + F1 T1 + F2 T2 + F3 T3 +} + +// Tuple4 is a structure carrying four elements +type Tuple4[T1, T2, T3, T4 any] struct { + F1 T1 + F2 T2 + F3 T3 + F4 T4 +} + +func MakeTuple1[T1 any](t1 T1) Tuple1[T1] { + return Tuple1[T1]{F1: t1} +} + +func MakeTuple2[T1, T2 any](t1 T1, t2 T2) Tuple2[T1, T2] { + return Tuple2[T1, T2]{F1: t1, F2: t2} +} + +func MakeTuple3[T1, T2, T3 any](t1 T1, t2 T2, t3 T3) Tuple3[T1, T2, T3] { + return Tuple3[T1, T2, T3]{F1: t1, F2: t2, F3: t3} +} + +func MakeTuple4[T1, T2, T3, T4 any](t1 T1, t2 T2, t3 T3, t4 T4) Tuple4[T1, T2, T3, T4] { + return Tuple4[T1, T2, T3, T4]{F1: t1, F2: t2, F3: t3, F4: t4} +} + +// Tupled2 converts a function that accepts two parameters into a function that accepts a tuple +func Tupled2[T1, T2, R any](f func(t1 T1, t2 T2) R) func(Tuple2[T1, T2]) R { + return func(t Tuple2[T1, T2]) R { + return f(t.F1, t.F2) + } +} + +// Tupled3 converts a function that accepts three parameters into a function that accepts a tuple +func Tupled3[T1, T2, T3, R any](f func(t1 T1, t2 T2, t3 T3) R) func(Tuple3[T1, T2, T3]) R { + return func(t Tuple3[T1, T2, T3]) R { + return f(t.F1, t.F2, t.F3) + } +} + +// Untupled2 converts a function that accepts a tuple into a function that accepts two parameters +func Untupled2[T1, T2, R any](f func(Tuple2[T1, T2]) R) func(T1, T2) R { + return func(t1 T1, t2 T2) R { + return f(MakeTuple2(t1, t2)) + } +} + +// Untupled3 converts a function that accepts a tuple into a function that accepts three parameters +func Untupled3[T1, T2, T3, R any](f func(Tuple3[T1, T2, T3]) R) func(T1, T2, T3) R { + return func(t1 T1, t2 T2, t3 T3) R { + return f(MakeTuple3(t1, t2, t3)) + } +} + +func First[T1, T2 any](t Tuple2[T1, T2]) T1 { + return t.F1 +} + +func Second[T1, T2 any](t Tuple2[T1, T2]) T2 { + return t.F2 +} + +func Swap[T1, T2 any](t Tuple2[T1, T2]) Tuple2[T2, T1] { + return MakeTuple2(t.F2, t.F1) +} + +func Of[T1, T2 any](e T2) func(T1) Tuple2[T1, T2] { + return func(t T1) Tuple2[T1, T2] { + return MakeTuple2(t, e) + } +} + +func BiMap[E, G, A, B any](mapSnd func(E) G, mapFst func(A) B) func(Tuple2[A, E]) Tuple2[B, G] { + return func(t Tuple2[A, E]) Tuple2[B, G] { + return MakeTuple2(mapFst(First(t)), mapSnd(Second(t))) + } +}