Skip to content

Commit

Permalink
fn: reimplement internals of either, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ProofOfKeags committed Apr 25, 2024
1 parent df51387 commit 09b6323
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 43 deletions.
76 changes: 49 additions & 27 deletions fn/either.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,136 @@ 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,
}
}
}

// 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,
}
}
}
139 changes: 139 additions & 0 deletions fn/either_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
20 changes: 4 additions & 16 deletions fn/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

0 comments on commit 09b6323

Please sign in to comment.