From 7a7f3bdb2c72fed3b0851f2bb28880d63641894c Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 12 Aug 2024 12:03:56 -0700 Subject: [PATCH 01/15] fn: fix UnwrapOrFail semantics --- fn/option.go | 15 +++++++-------- fn/option_test.go | 11 +++++++++++ fn/result.go | 12 ++++++------ fn/result_test.go | 11 +++++++++++ 4 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 fn/option_test.go create mode 100644 fn/result_test.go diff --git a/fn/option.go b/fn/option.go index 797f3a0ff..b1f2e237b 100644 --- a/fn/option.go +++ b/fn/option.go @@ -1,6 +1,10 @@ package fn -import "testing" +import ( + "testing" + + "github.com/stretchr/testify/require" +) // Option[A] represents a value which may or may not be there. This is very // often preferable to nil-able pointers. @@ -61,14 +65,9 @@ func (o Option[A]) UnwrapOrFunc(f func() A) A { func (o Option[A]) UnwrapOrFail(t *testing.T) A { t.Helper() - if o.isSome { - return o.some - } + require.True(t, o.isSome, "Option[%T] was None()", o.some) - t.Fatalf("Option[%T] was None()", o.some) - - var zero A - return zero + return o.some } // UnwrapOrErr is used to extract a value from an option, if the option is diff --git a/fn/option_test.go b/fn/option_test.go new file mode 100644 index 000000000..53a924827 --- /dev/null +++ b/fn/option_test.go @@ -0,0 +1,11 @@ +package fn + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptionUnwrapOrFail(t *testing.T) { + require.Equal(t, Some(1).UnwrapOrFail(t), 1) +} diff --git a/fn/result.go b/fn/result.go index 93d2dd7d6..e94675e6e 100644 --- a/fn/result.go +++ b/fn/result.go @@ -3,6 +3,8 @@ package fn import ( "fmt" "testing" + + "github.com/stretchr/testify/require" ) // Result represents a value that can either be a success (T) or an error. @@ -114,13 +116,11 @@ func (r Result[T]) UnwrapOrElse(f func() T) T { func (r Result[T]) UnwrapOrFail(t *testing.T) T { t.Helper() - if r.IsErr() { - t.Fatalf("Result[%T] contained error: %v", r.left, r.right) - } + require.True( + t, r.IsOk(), "Result[%T] contained error: %v", r.left, r.right, + ) - var zero T - - return zero + return r.left } // FlatMap applies a function that returns a Result to the success value if it diff --git a/fn/result_test.go b/fn/result_test.go new file mode 100644 index 000000000..38ebed84a --- /dev/null +++ b/fn/result_test.go @@ -0,0 +1,11 @@ +package fn + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestResultUnwrapOrFail(t *testing.T) { + require.Equal(t, Ok(1).UnwrapOrFail(t), 1) +} From a741c5417559d40099d44a6c09c0117ca08bc13b Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Tue, 6 Aug 2024 15:20:50 -0700 Subject: [PATCH 02/15] fn: add compound slice functions --- fn/slice.go | 94 ++++++++++++++++++ fn/slice_test.go | 252 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 342 insertions(+), 4 deletions(-) diff --git a/fn/slice.go b/fn/slice.go index 3b25b32a1..4dd4a2c58 100644 --- a/fn/slice.go +++ b/fn/slice.go @@ -65,6 +65,34 @@ func Filter[A any](pred Pred[A], s []A) []A { return res } +// FilterMap takes a function argument that optionally produces a value and +// returns a slice of the 'Some' return values. +func FilterMap[A, B any](as []A, f func(A) Option[B]) []B { + var bs []B + + for _, a := range as { + f(a).WhenSome(func(b B) { + bs = append(bs, b) + }) + } + + return bs +} + +// TrimNones takes a slice of Option values and returns a slice of the Some +// values in it. +func TrimNones[A any](as []Option[A]) []A { + var somes []A + + for _, a := range as { + a.WhenSome(func(b A) { + somes = append(somes, b) + }) + } + + return somes +} + // Foldl iterates through all members of the slice left to right and reduces // them pairwise with an accumulator value that is seeded with the seed value in // the argument. @@ -318,3 +346,69 @@ func Unsnoc[A any](items []A) Option[T2[[]A, A]] { func Len[A any](items []A) uint { return uint(len(items)) } + +// CollectOptions collects a list of Options into a single Option of the list of +// Some values in it. If there are any Nones present it will return None. +func CollectOptions[A any](options []Option[A]) Option[[]A] { + // We intentionally do a separate None checking pass here to avoid + // allocating a new slice for the values until we're sure we need to. + for _, r := range options { + if r.IsNone() { + return None[[]A]() + } + } + + // Now that we're sure we have no Nones, we can just do an unchecked + // index into the some value of the option. + return Some(Map(func(o Option[A]) A { return o.some }, options)) +} + +// CollectResults collects a list of Results into a single Result of the list of +// Ok values in it. If there are any errors present it will return the first +// error encountered. +func CollectResults[A any](results []Result[A]) Result[[]A] { + // We intentionally do a separate error checking pass here to avoid + // allocating a new slice for the results until we're sure we need to. + for _, r := range results { + if r.IsErr() { + return Err[[]A](r.right) + } + } + + // Now that we're sure we have no errors, we can just do an unchecked + // index into the left side of the result. + return Ok(Map(func(r Result[A]) A { return r.left }, results)) +} + +// TraverseOption traverses a slice of A values, applying the provided +// function to each, collecting the results into an Option of a slice of B +// values. If any of the results are None, the entire result is None. +func TraverseOption[A, B any](as []A, f func(A) Option[B]) Option[[]B] { + var bs []B + for _, a := range as { + b := f(a) + if b.IsNone() { + return None[[]B]() + } + bs = append(bs, b.some) + } + + return Some(bs) +} + +// TraverseResult traverses a slice of A values, applying the provided +// function to each, collecting the results into a Result of a slice of B +// values. If any of the results are Err, the entire result is the first +// error encountered. +func TraverseResult[A, B any](as []A, f func(A) Result[B]) Result[[]B] { + var bs []B + for _, a := range as { + b := f(a) + if b.IsErr() { + return Err[[]B](b.right) + } + bs = append(bs, b.left) + } + + return Ok(bs) +} diff --git a/fn/slice_test.go b/fn/slice_test.go index 86c870ff8..fae9bd361 100644 --- a/fn/slice_test.go +++ b/fn/slice_test.go @@ -2,6 +2,7 @@ package fn import ( "fmt" + "math/rand" "slices" "testing" "testing/quick" @@ -413,6 +414,122 @@ func TestPropTailDecrementsLength(t *testing.T) { require.NoError(t, quick.Check(f, nil)) } +func TestSingletonTailIsEmpty(t *testing.T) { + require.Equal(t, Tail([]int{1}), Some([]int{})) +} + +func TestSingletonInitIsEmpty(t *testing.T) { + require.Equal(t, Init([]int{1}), Some([]int{})) +} + +// TestPropAlwaysNoneEmptyFilterMap ensures the property that if we were to +// always return none from our filter function then we would end up with an +// empty slice. +func TestPropAlwaysNoneEmptyFilterMap(t *testing.T) { + f := func(s []int) bool { + filtered := FilterMap(s, Const[Option[int], int](None[int]())) + return len(filtered) == 0 + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropFilterMapSomeIdentity ensures that if the filter function is a +// trivial lift into Option space, then we will get back the original slice. +func TestPropFilterMapSomeIdentity(t *testing.T) { + f := func(s []int) bool { + filtered := FilterMap(s, Some[int]) + return slices.Equal(s, filtered) + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropFilterMapCantGrow ensures that regardless of the filter functions +// return values, we will never end up with a slice larger than the original. +func TestPropFilterMapCantGrow(t *testing.T) { + f := func(s []int) bool { + filterFunc := func(i int) Option[int] { + if rand.Int()%2 == 0 { + return None[int]() + } + + return Some(i + rand.Int()) + } + + return len(FilterMap(s, filterFunc)) <= len(s) + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropFilterMapBisectIdentity ensures that the concatenation of the +// FilterMaps is the same as the FilterMap of the concatenation. +func TestPropFilterMapBisectIdentity(t *testing.T) { + f := func(s []int) bool { + sz := len(s) + first := s[0 : sz/2] + second := s[sz/2 : sz] + + filterFunc := func(i int) Option[int] { + if i%2 == 0 { + return None[int]() + } + + return Some(i) + } + + firstFiltered := FilterMap(first, filterFunc) + secondFiltered := FilterMap(second, filterFunc) + allFiltered := FilterMap(s, filterFunc) + reassembled := slices.Concat(firstFiltered, secondFiltered) + + return slices.Equal(allFiltered, reassembled) + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestTraverseOkIdentity ensures that trivially lifting the elements of a slice +// via the Ok function during a Traverse is equivalent to just lifting the +// entire slice via the Ok function. +func TestPropTraverseOkIdentity(t *testing.T) { + f := func(s []int) bool { + traversed := TraverseResult(s, Ok[int]) + + traversedOk := traversed.UnwrapOrFail(t) + + return slices.Equal(s, traversedOk) + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropTraverseSingleErrEjection ensures that if the traverse function +// returns even a single error, then the entire Traverse will error. +func TestPropTraverseSingleErrEjection(t *testing.T) { + f := func(s []int, errIdx uint8) bool { + if len(s) == 0 { + return true + } + + errIdxMut := int(errIdx) % len(s) + f := func(i int) Result[int] { + if errIdxMut == 0 { + return Errf[int]("err") + } + + errIdxMut-- + + return Ok(i) + } + + return TraverseResult(s, f).IsErr() + } + + require.NoError(t, quick.Check(f, nil)) +} + func TestPropInitDecrementsLength(t *testing.T) { f := func(s []uint8) bool { if len(s) == 0 { @@ -425,10 +542,137 @@ func TestPropInitDecrementsLength(t *testing.T) { require.NoError(t, quick.Check(f, nil)) } -func TestSingletonTailIsEmpty(t *testing.T) { - require.Equal(t, Tail([]int{1}), Some([]int{})) +// TestPropTrimNonesEqualsFilterMapIden checks that if we use the Iden +// function when calling FilterMap on a slice of Options that we get the same +// result as we would if we called TrimNones on it. +func TestPropTrimNonesEqualsFilterMapIden(t *testing.T) { + f := func(s []uint8) bool { + withNones := make([]Option[uint8], len(s)) + for i, x := range s { + if x%3 == 0 { + withNones[i] = None[uint8]() + } else { + withNones[i] = Some(x) + } + } + + return slices.Equal( + FilterMap(withNones, Iden[Option[uint8]]), + TrimNones(withNones), + ) + } + + require.NoError(t, quick.Check(f, nil)) } -func TestSingletonInitIsEmpty(t *testing.T) { - require.Equal(t, Init([]int{1}), Some([]int{})) +// TestPropCollectResultsSingleErrEjection ensures that if there is even a +// single error in the batch, then CollectResults will return an error. +func TestPropCollectResultsSingleErrEjection(t *testing.T) { + f := func(s []int, errIdx uint8) bool { + if len(s) == 0 { + return true + } + + errIdxMut := int(errIdx) % len(s) + f := func(i int) Result[int] { + if errIdxMut == 0 { + return Errf[int]("err") + } + + errIdxMut-- + + return Ok(i) + } + + return CollectResults(Map(f, s)).IsErr() + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropCollectResultsNoErrUnwrap ensures that if there are no errors in the +// results then we end up with unwrapping all of the Results in the slice. +func TestPropCollectResultsNoErrUnwrap(t *testing.T) { + f := func(s []int) bool { + res := CollectResults(Map(Ok[int], s)) + return !res.isRight && slices.Equal(res.left, s) + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropTraverseSomeIdentity ensures that trivially lifting the elements of a +// slice via the Some function during a Traverse is equivalent to just lifting +// the entire slice via the Some function. +func TestPropTraverseSomeIdentity(t *testing.T) { + f := func(s []int) bool { + traversed := TraverseOption(s, Some[int]) + + traversedSome := traversed.UnwrapOrFail(t) + + return slices.Equal(s, traversedSome) + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestTraverseSingleNoneEjection ensures that if the traverse function returns +// even a single None, then the entire Traverse will return None. +func TestTraverseSingleNoneEjection(t *testing.T) { + f := func(s []int, errIdx uint8) bool { + if len(s) == 0 { + return true + } + + errIdxMut := int(errIdx) % len(s) + f := func(i int) Option[int] { + if errIdxMut == 0 { + return None[int]() + } + + errIdxMut-- + + return Some(i) + } + + return TraverseOption(s, f).IsNone() + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropCollectOptionsSingleNoneEjection ensures that if there is even a +// single None in the batch, then CollectOptions will return a None. +func TestPropCollectOptionsSingleNoneEjection(t *testing.T) { + f := func(s []int, errIdx uint8) bool { + if len(s) == 0 { + return true + } + + errIdxMut := int(errIdx) % len(s) + f := func(i int) Option[int] { + if errIdxMut == 0 { + return None[int]() + } + + errIdxMut-- + + return Some(i) + } + + return CollectOptions(Map(f, s)).IsNone() + } + + require.NoError(t, quick.Check(f, nil)) +} + +// TestPropCollectOptionsNoNoneUnwrap ensures that if there are no nones in the +// options then we end up with unwrapping all of the Options in the slice. +func TestPropCollectOptionsNoNoneUnwrap(t *testing.T) { + f := func(s []int) bool { + res := CollectOptions(Map(Some[int], s)) + return res.isSome && slices.Equal(res.some, s) + } + + require.NoError(t, quick.Check(f, nil)) } From 5faad77f33b0a9df39c5775ca531b9564c9570bb Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Tue, 6 Aug 2024 15:38:14 -0700 Subject: [PATCH 03/15] fn: add conversions between Result and Option --- fn/option.go | 19 +++++++++++++++++++ fn/option_test.go | 17 +++++++++++++++++ fn/result.go | 14 ++++++++++++++ fn/result_test.go | 8 ++++++++ 4 files changed, 58 insertions(+) diff --git a/fn/option.go b/fn/option.go index b1f2e237b..06007f781 100644 --- a/fn/option.go +++ b/fn/option.go @@ -1,6 +1,7 @@ package fn import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -232,3 +233,21 @@ func OptionToRight[O, L, R any](o Option[O], l L) Either[L, O] { return NewLeft[L, O](l) } + +// SomeToOk allows you to convert an Option value to a Result with your own +// error. If the Option contained a Some, then the supplied error is ignored +// and Some is converted to Ok. +func (o Option[A]) SomeToOk(err error) Result[A] { + return Result[A]{ + OptionToLeft[A, A, error](o, err), + } +} + +// SomeToOkf allows you to convert an Option value to a Result with your own +// error message. If the Option contains a Some, then the supplied message is +// ignored and Some is converted to Ok. +func (o Option[A]) SomeToOkf(errString string, args ...interface{}) Result[A] { + return Result[A]{ + OptionToLeft[A, A, error](o, fmt.Errorf(errString, args...)), + } +} diff --git a/fn/option_test.go b/fn/option_test.go index 53a924827..484132d69 100644 --- a/fn/option_test.go +++ b/fn/option_test.go @@ -1,6 +1,8 @@ package fn import ( + "errors" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -9,3 +11,18 @@ import ( func TestOptionUnwrapOrFail(t *testing.T) { require.Equal(t, Some(1).UnwrapOrFail(t), 1) } + +func TestSomeToOk(t *testing.T) { + err := errors.New("err") + require.Equal(t, Some(1).SomeToOk(err), Ok(1)) + require.Equal(t, None[uint8]().SomeToOk(err), Err[uint8](err)) +} + +func TestSomeToOkf(t *testing.T) { + errStr := "err" + require.Equal(t, Some(1).SomeToOkf(errStr), Ok(1)) + require.Equal( + t, None[uint8]().SomeToOkf(errStr), + Err[uint8](fmt.Errorf(errStr)), + ) +} diff --git a/fn/result.go b/fn/result.go index e94675e6e..c8de5aa6b 100644 --- a/fn/result.go +++ b/fn/result.go @@ -79,15 +79,29 @@ func (r Result[T]) MapErr(f func(error) error) Result[T] { } // Option returns the success value as an Option. +// +// Deprecated: Use OkToSome instead. func (r Result[T]) Option() Option[T] { return r.Either.LeftToOption() } +// OkToSome mutes the error value of the result. +func (r Result[T]) OkToSome() Option[T] { + return r.Either.LeftToOption() +} + // WhenResult executes the given function if the Result is a success. +// +// Deprecated: Use WhenOk instead. func (r Result[T]) WhenResult(f func(T)) { r.WhenLeft(f) } +// WhenOk executes the given function if the Result is a success. +func (r Result[T]) WhenOk(f func(T)) { + r.WhenLeft(f) +} + // WhenErr executes the given function if the Result is an error. func (r Result[T]) WhenErr(f func(error)) { r.WhenRight(f) diff --git a/fn/result_test.go b/fn/result_test.go index 38ebed84a..fd21e4443 100644 --- a/fn/result_test.go +++ b/fn/result_test.go @@ -1,6 +1,7 @@ package fn import ( + "errors" "testing" "github.com/stretchr/testify/require" @@ -9,3 +10,10 @@ import ( func TestResultUnwrapOrFail(t *testing.T) { require.Equal(t, Ok(1).UnwrapOrFail(t), 1) } + +func TestOkToSome(t *testing.T) { + require.Equal(t, Ok(1).OkToSome(), Some(1)) + require.Equal( + t, Err[uint8](errors.New("err")).OkToSome(), None[uint8](), + ) +} From 8971c4c3aec03357fd9e2bf608e6c4da63635037 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Tue, 13 Aug 2024 16:36:07 -0700 Subject: [PATCH 04/15] fn: add operations to Result API --- fn/result.go | 39 +++++++++++++++++++++++++++++++++++---- fn/result_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/fn/result.go b/fn/result.go index c8de5aa6b..5be5362ba 100644 --- a/fn/result.go +++ b/fn/result.go @@ -64,20 +64,37 @@ func (r Result[T]) IsErr() bool { return r.IsRight() } -// Map applies a function to the success value if it exists. +// Map applies an endomorphic function to the success value if it exists. +// +// Deprecated: Use MapOk instead. func (r Result[T]) Map(f func(T) T) Result[T] { return Result[T]{ MapLeft[T, error](f)(r.Either), } } -// MapErr applies a function to the error value if it exists. +// MapOk applies an endomorphic function to the success value if it exists. +func (r Result[T]) MapOk(f func(T) T) Result[T] { + return Result[T]{ + MapLeft[T, error](f)(r.Either), + } +} + +// MapErr applies an endomorphic function to the error value if it exists. func (r Result[T]) MapErr(f func(error) error) Result[T] { return Result[T]{ MapRight[T](f)(r.Either), } } +// MapOk applies a non-endomorphic function to the success value if it exists +// and returns a Result of the new type. +func MapOk[A, B any](f func(A) B) func(Result[A]) Result[B] { + return func(r Result[A]) Result[B] { + return Result[B]{MapLeft[A, error](f)(r.Either)} + } +} + // Option returns the success value as an Option. // // Deprecated: Use OkToSome instead. @@ -137,8 +154,22 @@ func (r Result[T]) UnwrapOrFail(t *testing.T) T { return r.left } -// FlatMap applies a function that returns a Result to the success value if it -// exists. +// FlattenResult takes a nested Result and joins the two functor layers into +// one. +func FlattenResult[A any](r Result[Result[A]]) Result[A] { + if r.IsErr() { + return Err[A](r.right) + } + + if r.left.IsErr() { + return Err[A](r.left.right) + } + + return r.left +} + +// FlatMap applies a kleisli endomorphic function that returns a Result to the +// success value if it exists. func (r Result[T]) FlatMap(f func(T) Result[T]) Result[T] { if r.IsOk() { return r diff --git a/fn/result_test.go b/fn/result_test.go index fd21e4443..bfbe8d9bd 100644 --- a/fn/result_test.go +++ b/fn/result_test.go @@ -2,7 +2,9 @@ package fn import ( "errors" + "fmt" "testing" + "testing/quick" "github.com/stretchr/testify/require" ) @@ -17,3 +19,29 @@ func TestOkToSome(t *testing.T) { t, Err[uint8](errors.New("err")).OkToSome(), None[uint8](), ) } + +func TestMapOk(t *testing.T) { + inc := func(i int) int { + return i + 1 + } + f := func(i int) bool { + ok := Ok(i) + return MapOk(inc)(ok) == Ok(inc(i)) + } + + require.NoError(t, quick.Check(f, nil)) +} + +func TestFlattenResult(t *testing.T) { + f := func(i int) bool { + e := fmt.Errorf("error") + + x := FlattenResult(Ok(Ok(i))) == Ok(i) + y := FlattenResult(Ok(Err[int](e))) == Err[int](e) + z := FlattenResult(Err[Result[int]](e)) == Err[int](e) + + return x && y && z + } + + require.NoError(t, quick.Check(f, nil)) +} From 5dec35426c9c315a6f1973b8db95570f2d58ceb3 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Tue, 13 Aug 2024 16:59:18 -0700 Subject: [PATCH 05/15] fn: add transpositions for Option and Result --- fn/option.go | 12 ++++++++++++ fn/option_test.go | 25 +++++++++++++++++++++++++ fn/result.go | 12 ++++++++++++ fn/result_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/fn/option.go b/fn/option.go index 06007f781..57245a9ba 100644 --- a/fn/option.go +++ b/fn/option.go @@ -251,3 +251,15 @@ func (o Option[A]) SomeToOkf(errString string, args ...interface{}) Result[A] { OptionToLeft[A, A, error](o, fmt.Errorf(errString, args...)), } } + +// TransposeOptRes transposes the Option[Result[A]] into a Result[Option[A]]. +// This has the effect of leaving an A value alone while inverting the Option +// and Result layers. If there is no internal A value, it will convert the +// non-success value to the proper one in the transposition. +func TransposeOptRes[A any](o Option[Result[A]]) Result[Option[A]] { + if o.IsNone() { + return Ok(None[A]()) + } + + return Result[Option[A]]{MapLeft[A, error](Some[A])(o.some.Either)} +} diff --git a/fn/option_test.go b/fn/option_test.go index 484132d69..69f6608d3 100644 --- a/fn/option_test.go +++ b/fn/option_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "testing" + "testing/quick" "github.com/stretchr/testify/require" ) @@ -26,3 +27,27 @@ func TestSomeToOkf(t *testing.T) { Err[uint8](fmt.Errorf(errStr)), ) } + +func TestPropTransposeOptResInverts(t *testing.T) { + f := func(i uint) bool { + var o Option[Result[uint]] + switch i % 3 { + case 0: + o = Some(Ok(i)) + case 1: + o = Some(Errf[uint]("error")) + case 2: + o = None[Result[uint]]() + default: + return false + } + + odd := TransposeOptRes(o) == + TransposeOptRes(TransposeResOpt(TransposeOptRes(o))) + even := TransposeResOpt(TransposeOptRes(o)) == o + + return odd && even + } + + require.NoError(t, quick.Check(f, nil)) +} diff --git a/fn/result.go b/fn/result.go index 5be5362ba..3fa6492d9 100644 --- a/fn/result.go +++ b/fn/result.go @@ -223,3 +223,15 @@ func AndThen2[A, B, C any](ra Result[A], rb Result[B], }) }) } + +// TransposeResOpt transposes the Result[Option[A]] into a Option[Result[A]]. +// This has the effect of leaving an A value alone while inverting the Result +// and Option layers. If there is no internal A value, it will convert the +// non-success value to the proper one in the transposition. +func TransposeResOpt[A any](r Result[Option[A]]) Option[Result[A]] { + if r.IsErr() { + return Some(Err[A](r.right)) + } + + return MapOption(Ok[A])(r.left) +} diff --git a/fn/result_test.go b/fn/result_test.go index bfbe8d9bd..5830b8214 100644 --- a/fn/result_test.go +++ b/fn/result_test.go @@ -45,3 +45,28 @@ func TestFlattenResult(t *testing.T) { require.NoError(t, quick.Check(f, nil)) } + +func TestPropTransposeResOptInverts(t *testing.T) { + f := func(i uint) bool { + var r Result[Option[uint]] + switch i % 3 { + case 0: + r = Ok(Some(i)) + case 1: + r = Ok(None[uint]()) + case 2: + r = Errf[Option[uint]]("error") + default: + return false + } + + odd := TransposeResOpt(TransposeOptRes(TransposeResOpt(r))) == + TransposeResOpt(r) + + even := TransposeOptRes(TransposeResOpt(r)) == r + + return odd && even + } + + require.NoError(t, quick.Check(f, nil)) +} From 9bbd327a102fde4769e5d290ea419078b74a0e2e Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 15:22:30 -0700 Subject: [PATCH 06/15] fn: add Sink to Result --- fn/result.go | 10 ++++++++++ fn/result_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/fn/result.go b/fn/result.go index 3fa6492d9..6328a7891 100644 --- a/fn/result.go +++ b/fn/result.go @@ -224,6 +224,16 @@ func AndThen2[A, B, C any](ra Result[A], rb Result[B], }) } +// Sink consumes a Result, either propagating its error or processing its +// success value with a function that can fail. +func (r Result[A]) Sink(f func(A) error) error { + if r.IsErr() { + return r.right + } + + return f(r.left) +} + // TransposeResOpt transposes the Result[Option[A]] into a Option[Result[A]]. // This has the effect of leaving an A value alone while inverting the Result // and Option layers. If there is no internal A value, it will convert the diff --git a/fn/result_test.go b/fn/result_test.go index 5830b8214..2b5d942a4 100644 --- a/fn/result_test.go +++ b/fn/result_test.go @@ -70,3 +70,29 @@ func TestPropTransposeResOptInverts(t *testing.T) { require.NoError(t, quick.Check(f, nil)) } + +func TestSinkOnErrNoContinutationCall(t *testing.T) { + called := false + res := Err[uint8](errors.New("err")).Sink( + func(a uint8) error { + called = true + return nil + }, + ) + + require.False(t, called) + require.NotNil(t, res) +} + +func TestSinkOnOkContinuationCall(t *testing.T) { + called := false + res := Ok(uint8(1)).Sink( + func(a uint8) error { + called = true + return nil + }, + ) + + require.True(t, called) + require.Nil(t, res) +} From a026d64c1b2db080cbb145fcdd910cac4f9fb2f8 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 12 Aug 2024 12:11:32 -0700 Subject: [PATCH 07/15] fn: breaking - fix type variables for better inference --- fn/either_test.go | 6 ++---- fn/fn.go | 2 +- fn/option.go | 8 ++++---- fn/slice_test.go | 2 +- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/fn/either_test.go b/fn/either_test.go index dca15f8d9..81af2bc6c 100644 --- a/fn/either_test.go +++ b/fn/either_test.go @@ -100,16 +100,14 @@ func TestPropToOptionIdentities(t *testing.T) { e = NewRight[int, string](s) r2O := e.RightToOption() == Some(s) - o2R := e == OptionToRight[string, int, string]( - Some(s), i, - ) + o2R := e == OptionToRight(Some(s), i) l2O := e.LeftToOption() == None[int]() return r2O && o2R && l2O } else { e = NewLeft[int, string](i) l2O := e.LeftToOption() == Some(i) - o2L := e == OptionToLeft[int, int](Some(i), s) + o2L := e == OptionToLeft(Some(i), s) r2O := e.RightToOption() == None[string]() return l2O && o2L && r2O diff --git a/fn/fn.go b/fn/fn.go index 147bf7daf..ea9190b7f 100644 --- a/fn/fn.go +++ b/fn/fn.go @@ -23,7 +23,7 @@ func Iden[A any](a A) A { // Const is a function that accepts an argument and returns a function that // always returns that value irrespective of the returned function's argument. // This is also quite useful in conjunction with higher order functions. -func Const[A, B any](a A) func(B) A { +func Const[B, A any](a A) func(B) A { return func(_ B) A { return a } diff --git a/fn/option.go b/fn/option.go index 57245a9ba..656be264d 100644 --- a/fn/option.go +++ b/fn/option.go @@ -216,7 +216,7 @@ func (o Option[A]) UnsafeFromSome() A { // OptionToLeft can be used to convert an Option value into an Either, by // providing the Right value that should be used if the Option value is None. -func OptionToLeft[O, L, R any](o Option[O], r R) Either[O, R] { +func OptionToLeft[O, R any](o Option[O], r R) Either[O, R] { if o.IsSome() { return NewLeft[O, R](o.some) } @@ -226,7 +226,7 @@ func OptionToLeft[O, L, R any](o Option[O], r R) Either[O, R] { // OptionToRight can be used to convert an Option value into an Either, by // providing the Left value that should be used if the Option value is None. -func OptionToRight[O, L, R any](o Option[O], l L) Either[L, O] { +func OptionToRight[O, L any](o Option[O], l L) Either[L, O] { if o.IsSome() { return NewRight[L, O](o.some) } @@ -239,7 +239,7 @@ func OptionToRight[O, L, R any](o Option[O], l L) Either[L, O] { // and Some is converted to Ok. func (o Option[A]) SomeToOk(err error) Result[A] { return Result[A]{ - OptionToLeft[A, A, error](o, err), + OptionToLeft(o, err), } } @@ -248,7 +248,7 @@ func (o Option[A]) SomeToOk(err error) Result[A] { // ignored and Some is converted to Ok. func (o Option[A]) SomeToOkf(errString string, args ...interface{}) Result[A] { return Result[A]{ - OptionToLeft[A, A, error](o, fmt.Errorf(errString, args...)), + OptionToLeft(o, fmt.Errorf(errString, args...)), } } diff --git a/fn/slice_test.go b/fn/slice_test.go index fae9bd361..18f7602ad 100644 --- a/fn/slice_test.go +++ b/fn/slice_test.go @@ -427,7 +427,7 @@ func TestSingletonInitIsEmpty(t *testing.T) { // empty slice. func TestPropAlwaysNoneEmptyFilterMap(t *testing.T) { f := func(s []int) bool { - filtered := FilterMap(s, Const[Option[int], int](None[int]())) + filtered := FilterMap(s, Const[int](None[int]())) return len(filtered) == 0 } From c6734ea01349e303f75302db2d3ffe1162e467fd Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 17:56:24 -0700 Subject: [PATCH 08/15] fn: breaking - reverse argument order in slice funcs --- fn/list_test.go | 2 +- fn/slice.go | 42 ++++++++++++++----------------- fn/slice_test.go | 64 ++++++++++++++++++++++++------------------------ 3 files changed, 52 insertions(+), 56 deletions(-) diff --git a/fn/list_test.go b/fn/list_test.go index efe2c5a92..a2a1261a0 100644 --- a/fn/list_test.go +++ b/fn/list_test.go @@ -743,7 +743,7 @@ func TestFilterIdempotence(t *testing.T) { filtered := l.Filter(pred) - filteredAgain := Filter(pred, filtered) + filteredAgain := Filter(filtered, pred) return slices.Equal(filtered, filteredAgain) }, diff --git a/fn/slice.go b/fn/slice.go index 4dd4a2c58..afd8e0f7f 100644 --- a/fn/slice.go +++ b/fn/slice.go @@ -17,7 +17,7 @@ type Number interface { // All returns true when the supplied predicate evaluates to true for all of // the values in the slice. -func All[A any](pred func(A) bool, s []A) bool { +func All[A any](s []A, pred Pred[A]) bool { for _, val := range s { if !pred(val) { return false @@ -29,7 +29,7 @@ func All[A any](pred func(A) bool, s []A) bool { // Any returns true when the supplied predicate evaluates to true for any of // the values in the slice. -func Any[A any](pred func(A) bool, s []A) bool { +func Any[A any](s []A, pred Pred[A]) bool { for _, val := range s { if pred(val) { return true @@ -41,7 +41,7 @@ func Any[A any](pred func(A) bool, s []A) bool { // Map applies the function argument to all members of the slice and returns a // slice of those return values. -func Map[A, B any](f func(A) B, s []A) []B { +func Map[A, B any](s []A, f func(A) B) []B { res := make([]B, 0, len(s)) for _, val := range s { @@ -53,7 +53,7 @@ func Map[A, B any](f func(A) B, s []A) []B { // Filter creates a new slice of values where all the members of the returned // slice pass the predicate that is supplied in the argument. -func Filter[A any](pred Pred[A], s []A) []A { +func Filter[A any](s []A, pred Pred[A]) []A { res := make([]A, 0) for _, val := range s { @@ -96,7 +96,7 @@ func TrimNones[A any](as []Option[A]) []A { // Foldl iterates through all members of the slice left to right and reduces // them pairwise with an accumulator value that is seeded with the seed value in // the argument. -func Foldl[A, B any](f func(B, A) B, seed B, s []A) B { +func Foldl[A, B any](seed B, s []A, f func(B, A) B) B { acc := seed for _, val := range s { @@ -108,7 +108,7 @@ func Foldl[A, B any](f func(B, A) B, seed B, s []A) B { // Foldr, is exactly like Foldl except that it iterates over the slice from // right to left. -func Foldr[A, B any](f func(A, B) B, seed B, s []A) B { +func Foldr[A, B any](seed B, s []A, f func(A, B) B) B { acc := seed for i := range s { @@ -120,7 +120,7 @@ func Foldr[A, B any](f func(A, B) B, seed B, s []A) B { // Find returns the first value that passes the supplied predicate, or None if // the value wasn't found. -func Find[A any](pred Pred[A], s []A) Option[A] { +func Find[A any](s []A, pred Pred[A]) Option[A] { for _, val := range s { if pred(val) { return Some(val) @@ -132,7 +132,7 @@ func Find[A any](pred Pred[A], s []A) Option[A] { // FindIdx returns the first value that passes the supplied predicate along with // its index in the slice. If no satisfactory value is found, None is returned. -func FindIdx[A any](pred Pred[A], s []A) Option[T2[int, A]] { +func FindIdx[A any](s []A, pred Pred[A]) Option[T2[int, A]] { for i, val := range s { if pred(val) { return Some(NewT2[int, A](i, val)) @@ -144,16 +144,14 @@ func FindIdx[A any](pred Pred[A], s []A) Option[T2[int, A]] { // Elem returns true if the element in the argument is found in the slice func Elem[A comparable](a A, s []A) bool { - return Any(Eq(a), s) + return Any(s, Eq(a)) } // Flatten takes a slice of slices and returns a concatenation of those slices. func Flatten[A any](s [][]A) []A { - sz := Foldr( - func(l []A, acc uint64) uint64 { - return uint64(len(l)) + acc - }, 0, s, - ) + sz := Foldr(0, s, func(l []A, acc uint64) uint64 { + return uint64(len(l)) + acc + }) res := make([]A, 0, sz) @@ -178,7 +176,7 @@ func Replicate[A any](n uint, val A) []A { // Span, applied to a predicate and a slice, returns two slices where the first // element is the longest prefix (possibly empty) of slice elements that // satisfy the predicate and second element is the remainder of the slice. -func Span[A any](pred func(A) bool, s []A) ([]A, []A) { +func Span[A any](s []A, pred Pred[A]) ([]A, []A) { for i := range s { if !pred(s[i]) { fst := make([]A, i) @@ -211,7 +209,7 @@ func SplitAt[A any](n uint, s []A) ([]A, []A) { // ZipWith combines slice elements with the same index using the function // argument, returning a slice of the results. -func ZipWith[A, B, C any](f func(A, B) C, a []A, b []B) []C { +func ZipWith[A, B, C any](a []A, b []B, f func(A, B) C) []C { var l uint if la, lb := len(a), len(b); la < lb { @@ -246,9 +244,9 @@ func SliceToMap[A any, K comparable, V any](s []A, keyFunc func(A) K, // Sum calculates the sum of a slice of numbers, `items`. func Sum[B Number](items []B) B { - return Foldl(func(a, b B) B { + return Foldl(0, items, func(a, b B) B { return a + b - }, 0, items) + }) } // HasDuplicates checks if the given slice contains any duplicate elements. @@ -261,9 +259,7 @@ func HasDuplicates[A comparable](items []A) bool { // ForEachConc maps the argument function over the slice, spawning a new // goroutine for each element in the slice and then awaits all results before // returning them. -func ForEachConc[A, B any](f func(A) B, - as []A) []B { - +func ForEachConc[A, B any](as []A, f func(A) B) []B { var wait sync.WaitGroup ctx := context.Background() @@ -360,7 +356,7 @@ func CollectOptions[A any](options []Option[A]) Option[[]A] { // Now that we're sure we have no Nones, we can just do an unchecked // index into the some value of the option. - return Some(Map(func(o Option[A]) A { return o.some }, options)) + return Some(Map(options, func(o Option[A]) A { return o.some })) } // CollectResults collects a list of Results into a single Result of the list of @@ -377,7 +373,7 @@ func CollectResults[A any](results []Result[A]) Result[[]A] { // Now that we're sure we have no errors, we can just do an unchecked // index into the left side of the result. - return Ok(Map(func(r Result[A]) A { return r.left }, results)) + return Ok(Map(results, func(r Result[A]) A { return r.left })) } // TraverseOption traverses a slice of A values, applying the provided diff --git a/fn/slice_test.go b/fn/slice_test.go index 18f7602ad..961196973 100644 --- a/fn/slice_test.go +++ b/fn/slice_test.go @@ -16,30 +16,30 @@ func odd(a int) bool { return a%2 != 0 } func TestAll(t *testing.T) { x := []int{0, 2, 4, 6, 8} - require.True(t, All(even, x)) - require.False(t, All(odd, x)) + require.True(t, All(x, even)) + require.False(t, All(x, odd)) y := []int{1, 3, 5, 7, 9} - require.False(t, All(even, y)) - require.True(t, All(odd, y)) + require.False(t, All(y, even)) + require.True(t, All(y, odd)) z := []int{0, 2, 4, 6, 9} - require.False(t, All(even, z)) - require.False(t, All(odd, z)) + require.False(t, All(z, even)) + require.False(t, All(z, odd)) } func TestAny(t *testing.T) { x := []int{1, 3, 5, 7, 9} - require.False(t, Any(even, x)) - require.True(t, Any(odd, x)) + require.False(t, Any(x, even)) + require.True(t, Any(x, odd)) y := []int{0, 3, 5, 7, 9} - require.True(t, Any(even, y)) - require.True(t, Any(odd, y)) + require.True(t, Any(y, even)) + require.True(t, Any(y, odd)) z := []int{0, 2, 4, 6, 8} - require.True(t, Any(even, z)) - require.False(t, Any(odd, z)) + require.True(t, Any(z, even)) + require.False(t, Any(z, odd)) } func TestMap(t *testing.T) { @@ -47,7 +47,7 @@ func TestMap(t *testing.T) { x := []int{0, 2, 4, 6, 8} - y := Map(inc, x) + y := Map(x, inc) z := []int{1, 3, 5, 7, 9} @@ -57,11 +57,11 @@ func TestMap(t *testing.T) { func TestFilter(t *testing.T) { x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} - y := Filter(even, x) + y := Filter(x, even) - require.True(t, All(even, y)) + require.True(t, All(y, even)) - z := Filter(odd, y) + z := Filter(y, odd) require.Zero(t, len(z)) } @@ -72,7 +72,7 @@ func TestFoldl(t *testing.T) { x := []int{0, 1, 2, 3, 4} - r := Foldl(stupid, seed, x) + r := Foldl(seed, x, stupid) require.True(t, slices.Equal(x, r)) } @@ -83,7 +83,7 @@ func TestFoldr(t *testing.T) { x := []int{0, 1, 2, 3, 4} - z := Foldr(stupid, seed, x) + z := Foldr(seed, x, stupid) slices.Reverse[[]int](x) @@ -96,9 +96,9 @@ func TestFind(t *testing.T) { div3 := func(a int) bool { return a%3 == 0 } div8 := func(a int) bool { return a%8 == 0 } - require.Equal(t, Find(div3, x), Some(12)) + require.Equal(t, Find(x, div3), Some(12)) - require.Equal(t, Find(div8, x), None[int]()) + require.Equal(t, Find(x, div8), None[int]()) } func TestFlatten(t *testing.T) { @@ -117,7 +117,7 @@ func TestSpan(t *testing.T) { x := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} lt5 := func(a int) bool { return a < 5 } - low, high := Span(lt5, x) + low, high := Span(x, lt5) require.True(t, slices.Equal(low, []int{0, 1, 2, 3, 4})) require.True(t, slices.Equal(high, []int{5, 6, 7, 8, 9})) @@ -135,7 +135,7 @@ func TestZipWith(t *testing.T) { eq := func(a, b int) bool { return a == b } x := []int{0, 1, 2, 3, 4} y := Replicate(5, 1) - z := ZipWith(eq, x, y) + z := ZipWith(x, y, eq) require.True(t, slices.Equal( z, []bool{false, true, false, false, false}, )) @@ -290,8 +290,8 @@ func TestHasDuplicates(t *testing.T) { func TestPropForEachConcMapIsomorphism(t *testing.T) { f := func(incSize int, s []int) bool { inc := func(i int) int { return i + incSize } - mapped := Map(inc, s) - conc := ForEachConc(inc, s) + mapped := Map(s, inc) + conc := ForEachConc(s, inc) return slices.Equal(mapped, conc) } @@ -319,7 +319,7 @@ func TestPropForEachConcOutperformsMapWhenExpensive(t *testing.T) { c := make(chan bool, 1) go func() { - Map(inc, s) + Map(s, inc) select { case c <- false: default: @@ -327,7 +327,7 @@ func TestPropForEachConcOutperformsMapWhenExpensive(t *testing.T) { }() go func() { - ForEachConc(inc, s) + ForEachConc(s, inc) select { case c <- true: default: @@ -352,14 +352,14 @@ func TestPropFindIdxFindIdentity(t *testing.T) { return i%div == mod } - foundIdx := FindIdx(pred, s) + foundIdx := FindIdx(s, pred) // onlyVal :: Option[T2[A, B]] -> Option[B] onlyVal := MapOption(func(t2 T2[int, uint8]) uint8 { return t2.Second() }) - valuesEqual := Find(pred, s) == onlyVal(foundIdx) + valuesEqual := Find(s, pred) == onlyVal(foundIdx) idxGetsVal := ElimOption( foundIdx, @@ -584,7 +584,7 @@ func TestPropCollectResultsSingleErrEjection(t *testing.T) { return Ok(i) } - return CollectResults(Map(f, s)).IsErr() + return CollectResults(Map(s, f)).IsErr() } require.NoError(t, quick.Check(f, nil)) @@ -594,7 +594,7 @@ func TestPropCollectResultsSingleErrEjection(t *testing.T) { // results then we end up with unwrapping all of the Results in the slice. func TestPropCollectResultsNoErrUnwrap(t *testing.T) { f := func(s []int) bool { - res := CollectResults(Map(Ok[int], s)) + res := CollectResults(Map(s, Ok[int])) return !res.isRight && slices.Equal(res.left, s) } @@ -660,7 +660,7 @@ func TestPropCollectOptionsSingleNoneEjection(t *testing.T) { return Some(i) } - return CollectOptions(Map(f, s)).IsNone() + return CollectOptions(Map(s, f)).IsNone() } require.NoError(t, quick.Check(f, nil)) @@ -670,7 +670,7 @@ func TestPropCollectOptionsSingleNoneEjection(t *testing.T) { // options then we end up with unwrapping all of the Options in the slice. func TestPropCollectOptionsNoNoneUnwrap(t *testing.T) { f := func(s []int) bool { - res := CollectOptions(Map(Some[int], s)) + res := CollectOptions(Map(s, Some[int])) return res.isSome && slices.Equal(res.some, s) } From 1805fd6f19a53778542e13f8421214dece5a6920 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 18:02:51 -0700 Subject: [PATCH 09/15] fn: breaking - polish Either API --- fn/either.go | 10 +++++----- fn/either_test.go | 12 ++++++------ fn/result.go | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/fn/either.go b/fn/either.go index f85104385..19a7ec171 100644 --- a/fn/either.go +++ b/fn/either.go @@ -20,7 +20,7 @@ func NewRight[L any, R any](r R) Either[L, R] { // ElimEither is the universal Either eliminator. It can be used to safely // handle all possible values inside the Either by supplying two continuations, // one for each side of the Either. -func ElimEither[L, R, O any](f func(L) O, g func(R) O, e Either[L, R]) O { +func ElimEither[L, R, O any](e Either[L, R], f func(L) O, g func(R) O) O { if !e.isRight { return f(e.left) } @@ -52,9 +52,9 @@ func (e Either[L, R]) IsRight() bool { return e.isRight } -// LeftToOption converts a Left value to an Option, returning None if the inner +// LeftToSome converts a Left value to an Option, returning None if the inner // Either value is a Right value. -func (e Either[L, R]) LeftToOption() Option[L] { +func (e Either[L, R]) LeftToSome() Option[L] { if e.isRight { return None[L]() } @@ -62,9 +62,9 @@ func (e Either[L, R]) LeftToOption() Option[L] { return Some(e.left) } -// RightToOption converts a Right value to an Option, returning None if the +// RightToSome converts a Right value to an Option, returning None if the // inner Either value is a Left value. -func (e Either[L, R]) RightToOption() Option[R] { +func (e Either[L, R]) RightToSome() Option[R] { if !e.isRight { return None[R]() } diff --git a/fn/either_test.go b/fn/either_test.go index 81af2bc6c..e8e29b6c1 100644 --- a/fn/either_test.go +++ b/fn/either_test.go @@ -10,17 +10,17 @@ func TestPropConstructorEliminatorDuality(t *testing.T) { Len := func(s string) int { return len(s) } // smh if isRight { v := ElimEither( + NewRight[int, string](s), Iden[int], Len, - NewRight[int, string](s), ) return v == Len(s) } v := ElimEither( + NewLeft[int, string](i), Iden[int], Len, - NewLeft[int, string](i), ) return v == i } @@ -99,16 +99,16 @@ func TestPropToOptionIdentities(t *testing.T) { if isRight { e = NewRight[int, string](s) - r2O := e.RightToOption() == Some(s) + r2O := e.RightToSome() == Some(s) o2R := e == OptionToRight(Some(s), i) - l2O := e.LeftToOption() == None[int]() + l2O := e.LeftToSome() == None[int]() return r2O && o2R && l2O } else { e = NewLeft[int, string](i) - l2O := e.LeftToOption() == Some(i) + l2O := e.LeftToSome() == Some(i) o2L := e == OptionToLeft(Some(i), s) - r2O := e.RightToOption() == None[string]() + r2O := e.RightToSome() == None[string]() return l2O && o2L && r2O } diff --git a/fn/result.go b/fn/result.go index 6328a7891..57c920039 100644 --- a/fn/result.go +++ b/fn/result.go @@ -99,12 +99,12 @@ func MapOk[A, B any](f func(A) B) func(Result[A]) Result[B] { // // Deprecated: Use OkToSome instead. func (r Result[T]) Option() Option[T] { - return r.Either.LeftToOption() + return r.Either.LeftToSome() } // OkToSome mutes the error value of the result. func (r Result[T]) OkToSome() Option[T] { - return r.Either.LeftToOption() + return r.Either.LeftToSome() } // WhenResult executes the given function if the Result is a success. From be50dd9acb8ca9b41ced07d1174b0178eee9f4af Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 18:04:14 -0700 Subject: [PATCH 10/15] fn: breaking - remove deprecated Result functions --- fn/result.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/fn/result.go b/fn/result.go index 57c920039..8d9c90812 100644 --- a/fn/result.go +++ b/fn/result.go @@ -64,15 +64,6 @@ func (r Result[T]) IsErr() bool { return r.IsRight() } -// Map applies an endomorphic function to the success value if it exists. -// -// Deprecated: Use MapOk instead. -func (r Result[T]) Map(f func(T) T) Result[T] { - return Result[T]{ - MapLeft[T, error](f)(r.Either), - } -} - // MapOk applies an endomorphic function to the success value if it exists. func (r Result[T]) MapOk(f func(T) T) Result[T] { return Result[T]{ @@ -95,25 +86,11 @@ func MapOk[A, B any](f func(A) B) func(Result[A]) Result[B] { } } -// Option returns the success value as an Option. -// -// Deprecated: Use OkToSome instead. -func (r Result[T]) Option() Option[T] { - return r.Either.LeftToSome() -} - // OkToSome mutes the error value of the result. func (r Result[T]) OkToSome() Option[T] { return r.Either.LeftToSome() } -// WhenResult executes the given function if the Result is a success. -// -// Deprecated: Use WhenOk instead. -func (r Result[T]) WhenResult(f func(T)) { - r.WhenLeft(f) -} - // WhenOk executes the given function if the Result is a success. func (r Result[T]) WhenOk(f func(T)) { r.WhenLeft(f) From 1480287cfc6b45a4c50e4a3941de126f8168bbd2 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 18:12:20 -0700 Subject: [PATCH 11/15] fn: breaking - improve naming of option api functions --- fn/either_test.go | 4 ++-- fn/option.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/fn/either_test.go b/fn/either_test.go index e8e29b6c1..e00891b54 100644 --- a/fn/either_test.go +++ b/fn/either_test.go @@ -100,14 +100,14 @@ func TestPropToOptionIdentities(t *testing.T) { e = NewRight[int, string](s) r2O := e.RightToSome() == Some(s) - o2R := e == OptionToRight(Some(s), i) + o2R := e == SomeToRight(Some(s), i) l2O := e.LeftToSome() == None[int]() return r2O && o2R && l2O } else { e = NewLeft[int, string](i) l2O := e.LeftToSome() == Some(i) - o2L := e == OptionToLeft(Some(i), s) + o2L := e == SomeToLeft(Some(i), s) r2O := e.RightToSome() == None[string]() return l2O && o2L && r2O diff --git a/fn/option.go b/fn/option.go index 656be264d..4d32d3e2e 100644 --- a/fn/option.go +++ b/fn/option.go @@ -214,9 +214,9 @@ func (o Option[A]) UnsafeFromSome() A { panic("Option was None()") } -// OptionToLeft can be used to convert an Option value into an Either, by +// SomeToLeft can be used to convert an Option value into an Either, by // providing the Right value that should be used if the Option value is None. -func OptionToLeft[O, R any](o Option[O], r R) Either[O, R] { +func SomeToLeft[O, R any](o Option[O], r R) Either[O, R] { if o.IsSome() { return NewLeft[O, R](o.some) } @@ -224,9 +224,9 @@ func OptionToLeft[O, R any](o Option[O], r R) Either[O, R] { return NewRight[O, R](r) } -// OptionToRight can be used to convert an Option value into an Either, by +// SomeToRight can be used to convert an Option value into an Either, by // providing the Left value that should be used if the Option value is None. -func OptionToRight[O, L any](o Option[O], l L) Either[L, O] { +func SomeToRight[O, L any](o Option[O], l L) Either[L, O] { if o.IsSome() { return NewRight[L, O](o.some) } @@ -239,7 +239,7 @@ func OptionToRight[O, L any](o Option[O], l L) Either[L, O] { // and Some is converted to Ok. func (o Option[A]) SomeToOk(err error) Result[A] { return Result[A]{ - OptionToLeft(o, err), + SomeToLeft(o, err), } } @@ -248,7 +248,7 @@ func (o Option[A]) SomeToOk(err error) Result[A] { // ignored and Some is converted to Ok. func (o Option[A]) SomeToOkf(errString string, args ...interface{}) Result[A] { return Result[A]{ - OptionToLeft(o, fmt.Errorf(errString, args...)), + SomeToLeft(o, fmt.Errorf(errString, args...)), } } From 4d16d5ff15d071c43f472a1b95f689da60235630 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 18:24:17 -0700 Subject: [PATCH 12/15] fn: breaking - replace AndThen2 with more principled LiftA2Result --- fn/result.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/fn/result.go b/fn/result.go index 8d9c90812..9f4dc1a6c 100644 --- a/fn/result.go +++ b/fn/result.go @@ -189,16 +189,22 @@ func AndThen[A, B any](r Result[A], f func(A) Result[B]) Result[B] { return FlatMap(r, f) } -// AndThen2 applies a function that returns a Result[C] to the success values -// of two Result types if both exist. -func AndThen2[A, B, C any](ra Result[A], rb Result[B], - f func(A, B) Result[C]) Result[C] { +// LiftA2Result lifts a two-argument function to a function that can operate +// over results of its arguments. +func LiftA2Result[A, B, C any](f func(A, B) C, +) func(Result[A], Result[B]) Result[C] { - return AndThen(ra, func(a A) Result[C] { - return AndThen(rb, func(b B) Result[C] { - return f(a, b) - }) - }) + return func(ra Result[A], rb Result[B]) Result[C] { + if ra.IsErr() { + return Err[C](ra.right) + } + + if rb.IsErr() { + return Err[C](rb.right) + } + + return Ok(f(ra.left, rb.left)) + } } // Sink consumes a Result, either propagating its error or processing its From 5e947046a3483b270db077e5ba77994cc6110733 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 18:25:35 -0700 Subject: [PATCH 13/15] fn: breaking - give Result's FlatMap proper functor annotation --- fn/result.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fn/result.go b/fn/result.go index 9f4dc1a6c..15118c66d 100644 --- a/fn/result.go +++ b/fn/result.go @@ -173,9 +173,9 @@ func (r Result[T]) OrElse(f func() Result[T]) Result[T] { return f() } -// FlatMap applies a function that returns a Result[B] to the success value if -// it exists. -func FlatMap[A, B any](r Result[A], f func(A) Result[B]) Result[B] { +// FlatMapResult applies a function that returns a Result[B] to the success +// value if it exists. +func FlatMapResult[A, B any](r Result[A], f func(A) Result[B]) Result[B] { if r.IsOk() { return f(r.left) } @@ -186,7 +186,7 @@ func FlatMap[A, B any](r Result[A], f func(A) Result[B]) Result[B] { // AndThen is an alias for FlatMap. This along with OrElse can be used to // Railway Oriented Programming (ROP). func AndThen[A, B any](r Result[A], f func(A) Result[B]) Result[B] { - return FlatMap(r, f) + return FlatMapResult(r, f) } // LiftA2Result lifts a two-argument function to a function that can operate From dd7f1569c77b100646a6cfc291237b7536b69fab Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 18:26:20 -0700 Subject: [PATCH 14/15] fn: breaking - rename ChainOption to FlatMapOption for consistency --- fn/option.go | 6 +++--- fn/slice_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fn/option.go b/fn/option.go index 4d32d3e2e..f02f82c8a 100644 --- a/fn/option.go +++ b/fn/option.go @@ -133,11 +133,11 @@ func FlattenOption[A any](oo Option[Option[A]]) Option[A] { return oo.some } -// ChainOption transforms a function A -> Option[B] into one that accepts an +// FlatMapOption transforms a function A -> Option[B] into one that accepts an // Option[A] as an argument. // -// ChainOption : (A -> Option[B]) -> Option[A] -> Option[B]. -func ChainOption[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] { +// FlatMapOption : (A -> Option[B]) -> Option[A] -> Option[B]. +func FlatMapOption[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] { return func(o Option[A]) Option[B] { if o.isSome { return f(o.some) diff --git a/fn/slice_test.go b/fn/slice_test.go index 961196973..7269f801e 100644 --- a/fn/slice_test.go +++ b/fn/slice_test.go @@ -383,7 +383,7 @@ func TestPropLastTailIsLast(t *testing.T) { return true } - return Last(s) == ChainOption(Last[uint8])(Tail(s)) + return Last(s) == FlatMapOption(Last[uint8])(Tail(s)) } require.NoError(t, quick.Check(f, nil)) @@ -396,7 +396,7 @@ func TestPropHeadInitIsHead(t *testing.T) { return true } - return Head(s) == ChainOption(Head[uint8])(Init(s)) + return Head(s) == FlatMapOption(Head[uint8])(Init(s)) } require.NoError(t, quick.Check(f, nil)) From 9f35664a12474c127abad3d39b9c468650098ffc Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Wed, 14 Aug 2024 18:40:53 -0700 Subject: [PATCH 15/15] fn: breaking - make or else functions accept error argument --- fn/result.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fn/result.go b/fn/result.go index 15118c66d..37958f26c 100644 --- a/fn/result.go +++ b/fn/result.go @@ -112,9 +112,9 @@ func (r Result[T]) UnwrapOr(defaultValue T) T { // UnwrapOrElse returns the success value or computes a value from a function // if it's an error. -func (r Result[T]) UnwrapOrElse(f func() T) T { +func (r Result[T]) UnwrapOrElse(f func(error) T) T { if r.IsErr() { - return f() + return f(r.right) } return r.left @@ -165,12 +165,12 @@ func (r Result[T]) AndThen(f func(T) Result[T]) Result[T] { // OrElse returns the original Result if it is a success, otherwise it returns // the provided alternative Result. This along with AndThen can be used to // Railway Oriented Programming (ROP). -func (r Result[T]) OrElse(f func() Result[T]) Result[T] { +func (r Result[T]) OrElse(f func(error) Result[T]) Result[T] { if r.IsOk() { return r } - return f() + return f(r.right) } // FlatMapResult applies a function that returns a Result[B] to the success