diff --git a/fn/either.go b/fn/either.go index f85104385e2..19a7ec171e0 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 dca15f8d9fc..e00891b5473 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,18 +99,16 @@ func TestPropToOptionIdentities(t *testing.T) { if isRight { e = NewRight[int, string](s) - r2O := e.RightToOption() == Some(s) - o2R := e == OptionToRight[string, int, string]( - Some(s), i, - ) - l2O := e.LeftToOption() == None[int]() + r2O := e.RightToSome() == Some(s) + o2R := e == SomeToRight(Some(s), i) + l2O := e.LeftToSome() == 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) - r2O := e.RightToOption() == None[string]() + l2O := e.LeftToSome() == Some(i) + o2L := e == SomeToLeft(Some(i), s) + r2O := e.RightToSome() == None[string]() return l2O && o2L && r2O } diff --git a/fn/fn.go b/fn/fn.go index 147bf7daf44..ea9190b7f62 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/list_test.go b/fn/list_test.go index efe2c5a92ca..a2a1261a0ad 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/option.go b/fn/option.go index 797f3a0ff0d..f02f82c8ad9 100644 --- a/fn/option.go +++ b/fn/option.go @@ -1,6 +1,11 @@ package fn -import "testing" +import ( + "fmt" + "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 +66,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 - } - - t.Fatalf("Option[%T] was None()", o.some) + require.True(t, o.isSome, "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 @@ -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) @@ -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, L, 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,12 +224,42 @@ func OptionToLeft[O, L, 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, R 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) } 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]{ + SomeToLeft(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]{ + SomeToLeft(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 new file mode 100644 index 00000000000..69f6608d319 --- /dev/null +++ b/fn/option_test.go @@ -0,0 +1,53 @@ +package fn + +import ( + "errors" + "fmt" + "testing" + "testing/quick" + + "github.com/stretchr/testify/require" +) + +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)), + ) +} + +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 93d2dd7d66b..37958f26cd4 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. @@ -62,27 +64,35 @@ func (r Result[T]) IsErr() bool { return r.IsRight() } -// Map applies a function to the success value if it exists. -func (r Result[T]) Map(f func(T) T) Result[T] { +// 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 a function to the error value if it exists. +// 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), } } -// Option returns the success value as an Option. -func (r Result[T]) Option() Option[T] { - return r.Either.LeftToOption() +// 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)} + } +} + +// 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. -func (r Result[T]) WhenResult(f func(T)) { +// WhenOk executes the given function if the Result is a success. +func (r Result[T]) WhenOk(f func(T)) { r.WhenLeft(f) } @@ -102,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 @@ -114,17 +124,29 @@ func (r Result[T]) UnwrapOrElse(f func() T) T { func (r Result[T]) UnwrapOrFail(t *testing.T) T { t.Helper() + require.True( + t, r.IsOk(), "Result[%T] contained error: %v", r.left, r.right, + ) + + return r.left +} + +// 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() { - t.Fatalf("Result[%T] contained error: %v", r.left, r.right) + return Err[A](r.right) } - var zero T + if r.left.IsErr() { + return Err[A](r.left.right) + } - return zero + return r.left } -// FlatMap applies a function that returns a Result to the success value if it -// exists. +// 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 @@ -143,17 +165,17 @@ 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) } -// 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) } @@ -164,17 +186,45 @@ 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) } -// 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 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 +// 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 +// 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 AndThen(ra, func(a A) Result[C] { - return AndThen(rb, func(b B) Result[C] { - return f(a, b) - }) - }) + return MapOption(Ok[A])(r.left) } diff --git a/fn/result_test.go b/fn/result_test.go new file mode 100644 index 00000000000..2b5d942a4f6 --- /dev/null +++ b/fn/result_test.go @@ -0,0 +1,98 @@ +package fn + +import ( + "errors" + "fmt" + "testing" + "testing/quick" + + "github.com/stretchr/testify/require" +) + +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](), + ) +} + +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)) +} + +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)) +} + +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) +} diff --git a/fn/slice.go b/fn/slice.go index 3b25b32a1ae..afd8e0f7fb6 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 { @@ -65,10 +65,38 @@ 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. -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 { @@ -80,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 { @@ -92,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) @@ -104,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)) @@ -116,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) @@ -150,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) @@ -183,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 { @@ -218,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. @@ -233,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() @@ -318,3 +342,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(options, func(o Option[A]) A { return o.some })) +} + +// 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(results, func(r Result[A]) A { return r.left })) +} + +// 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 86c870ff8e6..7269f801ecd 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" @@ -15,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) { @@ -46,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} @@ -56,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)) } @@ -71,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)) } @@ -82,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) @@ -95,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) { @@ -116,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})) @@ -134,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}, )) @@ -289,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) } @@ -318,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: @@ -326,7 +327,7 @@ func TestPropForEachConcOutperformsMapWhenExpensive(t *testing.T) { }() go func() { - ForEachConc(inc, s) + ForEachConc(s, inc) select { case c <- true: default: @@ -351,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, @@ -382,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)) @@ -395,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)) @@ -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[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(s, f)).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(s, Ok[int])) + 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(s, f)).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(s, Some[int])) + return res.isSome && slices.Equal(res.some, s) + } + + require.NoError(t, quick.Check(f, nil)) }