From c4df2f1dce6ee4a7f4a33d66bcf451b9d3e51a95 Mon Sep 17 00:00:00 2001 From: Keagan McClelland Date: Mon, 22 Apr 2024 13:52:19 -0700 Subject: [PATCH] fn: reimplement internals of either, add tests --- fn/either.go | 76 ++++++++++++++++--------- fn/either_test.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++ fn/option.go | 20 ++----- fn/result.go | 60 ++++++++++++-------- 4 files changed, 228 insertions(+), 67 deletions(-) create mode 100644 fn/either_test.go diff --git a/fn/either.go b/fn/either.go index cb4555f17..f85104385 100644 --- a/fn/either.go +++ b/fn/either.go @@ -2,97 +2,119 @@ package fn // Either is a type that can be either left or right. type Either[L any, R any] struct { - left Option[L] - right Option[R] + isRight bool + left L + right R } // NewLeft returns an Either with a left value. func NewLeft[L any, R any](l L) Either[L, R] { - return Either[L, R]{left: Some(l), right: None[R]()} + return Either[L, R]{left: l} } // NewRight returns an Either with a right value. func NewRight[L any, R any](r R) Either[L, R] { - return Either[L, R]{left: None[L](), right: Some(r)} + return Either[L, R]{isRight: true, right: 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 { - if e.left.IsSome() { - return f(e.left.some) + if !e.isRight { + return f(e.left) } - return g(e.right.some) + return g(e.right) } // WhenLeft executes the given function if the Either is left. func (e Either[L, R]) WhenLeft(f func(L)) { - e.left.WhenSome(f) + if !e.isRight { + f(e.left) + } } // WhenRight executes the given function if the Either is right. func (e Either[L, R]) WhenRight(f func(R)) { - e.right.WhenSome(f) + if e.isRight { + f(e.right) + } } // IsLeft returns true if the Either is left. func (e Either[L, R]) IsLeft() bool { - return e.left.IsSome() + return !e.isRight } // IsRight returns true if the Either is right. func (e Either[L, R]) IsRight() bool { - return e.right.IsSome() + return e.isRight } // LeftToOption 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] { - return e.left + if e.isRight { + return None[L]() + } + + return Some(e.left) } // RightToOption 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] { - return e.right + if !e.isRight { + return None[R]() + } + + return Some(e.right) } // UnwrapLeftOr will extract the Left value from the Either if it is present // returning the supplied default if it is not. func (e Either[L, R]) UnwrapLeftOr(l L) L { - return e.left.UnwrapOr(l) + if e.isRight { + return l + } + + return e.left } // UnwrapRightOr will extract the Right value from the Either if it is present // returning the supplied default if it is not. func (e Either[L, R]) UnwrapRightOr(r R) R { - return e.right.UnwrapOr(r) + if !e.isRight { + return r + } + + return e.right } // Swap reverses the type argument order. This can be useful as an adapter // between APIs. func (e Either[L, R]) Swap() Either[R, L] { return Either[R, L]{ - left: e.right, - right: e.left, + isRight: !e.isRight, + left: e.right, + right: e.left, } } // MapLeft maps the left value of the Either to a new value. func MapLeft[L, R, O any](f func(L) O) func(Either[L, R]) Either[O, R] { return func(e Either[L, R]) Either[O, R] { - if e.IsLeft() { + if !e.isRight { return Either[O, R]{ - left: MapOption(f)(e.left), - right: None[R](), + isRight: false, + left: f(e.left), } } return Either[O, R]{ - left: None[O](), - right: e.right, + isRight: true, + right: e.right, } } } @@ -100,16 +122,16 @@ func MapLeft[L, R, O any](f func(L) O) func(Either[L, R]) Either[O, R] { // MapRight maps the right value of the Either to a new value. func MapRight[L, R, O any](f func(R) O) func(Either[L, R]) Either[L, O] { return func(e Either[L, R]) Either[L, O] { - if e.IsRight() { + if e.isRight { return Either[L, O]{ - left: None[L](), - right: MapOption(f)(e.right), + isRight: true, + right: f(e.right), } } return Either[L, O]{ - left: e.left, - right: None[O](), + isRight: false, + left: e.left, } } } diff --git a/fn/either_test.go b/fn/either_test.go new file mode 100644 index 000000000..dca15f8d9 --- /dev/null +++ b/fn/either_test.go @@ -0,0 +1,139 @@ +package fn + +import ( + "testing" + "testing/quick" +) + +func TestPropConstructorEliminatorDuality(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + Len := func(s string) int { return len(s) } // smh + if isRight { + v := ElimEither( + Iden[int], + Len, + NewRight[int, string](s), + ) + return v == Len(s) + } + + v := ElimEither( + Iden[int], + Len, + NewLeft[int, string](i), + ) + return v == i + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropWhenClauseExclusivity(t *testing.T) { + f := func(i int, isRight bool) bool { + var e Either[int, int] + if isRight { + e = NewRight[int, int](i) + } else { + e = NewLeft[int, int](i) + } + z := 0 + e.WhenLeft(func(x int) { z += x }) + e.WhenRight(func(x int) { z += x }) + + return z != 2*i && e.IsLeft() != e.IsRight() + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropSwapEitherSelfInverting(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + } else { + e = NewLeft[int, string](i) + } + return e.Swap().Swap() == e + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropMapLeftIdentity(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + } else { + e = NewLeft[int, string](i) + } + return MapLeft[int, string, int](Iden[int])(e) == e + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropMapRightIdentity(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + } else { + e = NewLeft[int, string](i) + } + return MapRight[int, string, string](Iden[string])(e) == e + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropToOptionIdentities(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + 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]() + + 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]() + + return l2O && o2L && r2O + } + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} + +func TestPropUnwrapIdentities(t *testing.T) { + f := func(i int, s string, isRight bool) bool { + var e Either[int, string] + if isRight { + e = NewRight[int, string](s) + return e.UnwrapRightOr("") == s && + e.UnwrapLeftOr(0) == 0 + } else { + e = NewLeft[int, string](i) + return e.UnwrapLeftOr(0) == i && + e.UnwrapRightOr("") == "" + } + } + if err := quick.Check(f, nil); err != nil { + t.Fatal(err) + } +} diff --git a/fn/option.go b/fn/option.go index fbd5f2489..797f3a0ff 100644 --- a/fn/option.go +++ b/fn/option.go @@ -218,30 +218,18 @@ func (o Option[A]) UnsafeFromSome() A { // 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] { if o.IsSome() { - return Either[O, R]{ - left: o, - right: None[R](), - } + return NewLeft[O, R](o.some) } - return Either[O, R]{ - left: None[O](), - right: Some(r), - } + return NewRight[O, R](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] { if o.IsSome() { - return Either[L, O]{ - left: None[L](), - right: o, - } + return NewRight[L, O](o.some) } - return Either[L, O]{ - left: Some(l), - right: None[O](), - } + return NewLeft[L, O](l) } diff --git a/fn/result.go b/fn/result.go index cb459bdc8..93d2dd7d6 100644 --- a/fn/result.go +++ b/fn/result.go @@ -39,12 +39,17 @@ func Errf[T any](errString string, args ...any) Result[T] { // Unpack extracts the value or error from the Result. func (r Result[T]) Unpack() (T, error) { var zero T - return r.left.UnwrapOr(zero), r.right.UnwrapOr(nil) + + if r.IsErr() { + return zero, r.right + } + + return r.left, nil } // Err exposes the underlying error of the result type as a normal error type. func (r Result[T]) Err() error { - return r.right.some + return r.right } // IsOk returns true if the Result is a success value. @@ -59,66 +64,73 @@ func (r Result[T]) IsErr() bool { // Map applies a function to the success value if it exists. func (r Result[T]) Map(f func(T) T) Result[T] { - if r.IsOk() { - return Ok(f(r.left.some)) + return Result[T]{ + MapLeft[T, error](f)(r.Either), } - - return r } // MapErr applies a function to the error value if it exists. func (r Result[T]) MapErr(f func(error) error) Result[T] { - if r.IsErr() { - return Err[T](f(r.right.some)) + return Result[T]{ + MapRight[T](f)(r.Either), } - - return r } // Option returns the success value as an Option. func (r Result[T]) Option() Option[T] { - return r.left + return r.Either.LeftToOption() } // WhenResult executes the given function if the Result is a success. func (r Result[T]) WhenResult(f func(T)) { - r.left.WhenSome(func(t T) { - f(t) - }) + r.WhenLeft(f) } // WhenErr executes the given function if the Result is an error. func (r Result[T]) WhenErr(f func(error)) { - r.right.WhenSome(func(e error) { - f(e) - }) + r.WhenRight(f) } // UnwrapOr returns the success value or a default value if it's an error. func (r Result[T]) UnwrapOr(defaultValue T) T { - return r.left.UnwrapOr(defaultValue) + if r.IsErr() { + return defaultValue + } + + return r.left } // 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 { - return r.left.UnwrapOrFunc(f) + if r.IsErr() { + return f() + } + + return r.left } // UnwrapOrFail returns the success value or fails the test if it's an error. func (r Result[T]) UnwrapOrFail(t *testing.T) T { t.Helper() - return r.left.UnwrapOrFail(t) + if r.IsErr() { + t.Fatalf("Result[%T] contained error: %v", r.left, r.right) + } + + var zero T + + return zero } // FlatMap applies a 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 f(r.left.some) + return r } - return r + + return f(r.left) } // AndThen is an alias for FlatMap. This along with OrElse can be used to @@ -143,10 +155,10 @@ func (r Result[T]) OrElse(f func() Result[T]) Result[T] { // it exists. func FlatMap[A, B any](r Result[A], f func(A) Result[B]) Result[B] { if r.IsOk() { - return f(r.left.some) + return f(r.left) } - return Err[B](r.right.some) + return Err[B](r.right) } // AndThen is an alias for FlatMap. This along with OrElse can be used to