Merge pull request #8985 from ProofOfKeags/fn/collect-results

fn: more fn goodies
This commit is contained in:
Oliver Gugger 2024-11-07 18:50:57 +01:00 committed by GitHub
commit f42636fec9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 683 additions and 120 deletions

View File

@ -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]()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
},

View File

@ -1,6 +1,11 @@
package fn
import "testing"
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
// Option represents a value which may or may not be there. This is very often
// preferable to nil-able pointers.
@ -72,14 +77,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
@ -144,11 +144,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)
@ -225,9 +225,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)
}
@ -235,12 +235,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)}
}

53
fn/option_test.go Normal file
View File

@ -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))
}

View File

@ -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)}
}
}
// WhenResult executes the given function if the Result is a success.
func (r Result[T]) WhenResult(f func(T)) {
// OkToSome mutes the error value of the result.
func (r Result[T]) OkToSome() Option[T] {
return r.Either.LeftToSome()
}
// 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()
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
// 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
@ -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 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
// 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 MapOption(Ok[A])(r.left)
}

98
fn/result_test.go Normal file
View File

@ -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)
}

View File

@ -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)
}

View File

@ -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))
}