From 67e56cefabdfc0d6ce4d5b2211ade06192e22bff Mon Sep 17 00:00:00 2001 From: Steven vanZyl Date: Mon, 4 Nov 2024 16:44:40 -0500 Subject: [PATCH] interface is back --- {threading => fun}/chain.go | 2 +- {threading => fun}/chain_test.go | 2 +- {threading => fun}/curry.go | 2 +- {threading => fun}/curry_test.go | 2 +- fun/fun.go | 23 ++++++++ generators/generators.go | 37 ++++++------ generators/generators_test.go | 4 +- iter.go | 49 ++++++++++++++++ magic.go | 45 ++++++++------- monads/monads.go | 8 ++- monads/option.go | 4 ++ monads/result.go | 4 ++ reducers/reducers.go | 36 +++++------- transducers/meta.go | 12 ++-- transducers/transducers.go | 99 ++++++++++++++++++-------------- transducers/transducers_test.go | 15 ++--- 16 files changed, 216 insertions(+), 128 deletions(-) rename {threading => fun}/chain.go (98%) rename {threading => fun}/chain_test.go (97%) rename {threading => fun}/curry.go (98%) rename {threading => fun}/curry_test.go (96%) create mode 100644 fun/fun.go create mode 100644 iter.go diff --git a/threading/chain.go b/fun/chain.go similarity index 98% rename from threading/chain.go rename to fun/chain.go index dd78e7f..4919161 100644 --- a/threading/chain.go +++ b/fun/chain.go @@ -1,4 +1,4 @@ -package threading +package fun import ( "fmt" diff --git a/threading/chain_test.go b/fun/chain_test.go similarity index 97% rename from threading/chain_test.go rename to fun/chain_test.go index 27ae825..95db044 100644 --- a/threading/chain_test.go +++ b/fun/chain_test.go @@ -1,4 +1,4 @@ -package threading +package fun import ( "strconv" diff --git a/threading/curry.go b/fun/curry.go similarity index 98% rename from threading/curry.go rename to fun/curry.go index 24bea05..21b41f5 100644 --- a/threading/curry.go +++ b/fun/curry.go @@ -1,4 +1,4 @@ -package threading +package fun import ( "fmt" diff --git a/threading/curry_test.go b/fun/curry_test.go similarity index 96% rename from threading/curry_test.go rename to fun/curry_test.go index 7862775..dc6aa21 100644 --- a/threading/curry_test.go +++ b/fun/curry_test.go @@ -1,4 +1,4 @@ -package threading +package fun import "testing" diff --git a/fun/fun.go b/fun/fun.go new file mode 100644 index 0000000..d05ede4 --- /dev/null +++ b/fun/fun.go @@ -0,0 +1,23 @@ +package fun + +import "github.com/rushsteve1/fp" + +func Identity[T any](t T) T { + return t +} + + +// Errorless takes a function that can error and returns a new function that +// wraps the provided one in [fp.Must] +func Errorless[T, U any](f func(T) (U, error)) func(T) U { + return func(t T) U { + return fp.Must(f(t)) + } +} + +// Discard takes a function and returns a new wrapping function without the return value +func Discard[T, U any](f func(T) U) func(T) { + return func(t T) { + _ = f(t) + } +} \ No newline at end of file diff --git a/generators/generators.go b/generators/generators.go index 4316352..f23d8e0 100644 --- a/generators/generators.go +++ b/generators/generators.go @@ -2,96 +2,95 @@ package generators import ( "io" - . "iter" "net" "time" - "github.com/rushsteve1/fp" + . "github.com/rushsteve1/fp" ) // A generator is any function that returns a new sequence // Empty returns an infinite empty sequence that never yields any values func Empty[T any]() Seq[T] { - return func(func(T) bool){} + return SeqFunc[T](func(func(T) bool){}) } // Generate returns an iterator that repeatedly calls the provided function, // yielding the values it returns func Generate[T any](f func() T) Seq[T] { - return func(yield func(T) bool) { + return SeqFunc[T](func(yield func(T) bool) { if !yield(f()) { return } - } + }) } // Forever returns an infinite sequence of the provided value func Forever[T any](v T) Seq[T] { - return func(yield func(T) bool){ + return SeqFunc[T](func(yield func(T) bool){ if !yield(v) { return } - } + }) } // Integers yields an infinite sequence of integers func Integers() Seq[int] { - return func(yield func(int) bool) { + return SeqFunc[int](func(yield func(int) bool) { n := 0 for yield(n) { n++ } - } + }) } // Ticker yields the current time when the duration has passed func Ticker(d time.Duration) Seq[time.Time] { - return func(yield func(time.Time) bool) { + return SeqFunc[time.Time](func(yield func(time.Time) bool) { for t := range time.Tick(d) { if !yield(t) { return } } - } + }) } // Chan returns an iterator that continually yields values from the channel. // The channel is closed if the sequence stops. // It is the caller's responsibility to close the channel to prevent deadlocks func Chan[T any](c chan T) Seq[T] { - return func(yield func(T) bool) { + return SeqFunc[T](func(yield func(T) bool) { for t := range c { if !yield(t) { close(c) return } } - } + }) } // Reader reads from the passed [io.Reader] turning into a sequence of byte arrays. // See its counterpart [transducers.Writer] func Reader(r io.Reader) Seq[[]byte] { - return func(yield func([]byte) bool) { + return SeqFunc[[]byte](func(yield func([]byte) bool) { var buf []byte - fp.Must(r.Read(buf)) + Must(r.Read(buf)) if !yield(buf) { return } - } + }) } // Accept takes a listener and returns a sequence of accepted connections. // The listener is closed if the sequence stops. func Accept(l net.Listener) Seq[net.Conn] { - return func(yield func(net.Conn) bool) { + return SeqFunc[net.Conn](func(yield func(net.Conn) bool) { for { - c := fp.Must(l.Accept()) + c := Must(l.Accept()) if !yield(c) { l.Close() return } } - } + }) } \ No newline at end of file diff --git a/generators/generators_test.go b/generators/generators_test.go index ff0e044..c6d08a9 100644 --- a/generators/generators_test.go +++ b/generators/generators_test.go @@ -9,7 +9,7 @@ import ( func TestTicker(t *testing.T) { i := 0 - for _ = range Ticker(time.Millisecond * 100) { + for _ = range Ticker(time.Millisecond * 100).Seq { if i > 5 { break } @@ -29,7 +29,7 @@ func TestChan(t *testing.T) { }() i := 0 - for v := range s { + for v := range s.Seq { t.Log(v) i++ } diff --git a/iter.go b/iter.go new file mode 100644 index 0000000..43b6b98 --- /dev/null +++ b/iter.go @@ -0,0 +1,49 @@ +// This package wraps the standard library's [iter] package, providing some +// additional features. +// +// It is intended to potentially inform future development and act as the +// backbone of this library. + +package fp + +import ( + "iter" +) + +// SeqFunc is exactly the same as [iter.Seq] and can be trivially cast between +type SeqFunc[V any] iter.Seq[V] + +// Seq borrows a trick used by [http.Handler] to define an interface and a func +// that implements that interface by calling itself [SeqFunc] +type Seq[V any] interface { + // Seq implements push-style iteration using the yield callback. + // See the documenation of [iter] for more information. + // It has exactly the same signature as [SeqFunc]. + Seq(yield func(V) bool) +} + +func (sf SeqFunc[V]) Seq(yield func(V) bool) { + sf(yield) +} + +// Seq2Func is exactly the same as [iter.Seq2] and can be trivially cast between +type Seq2Func[K, V any] iter.Seq2[K, V] + +// Seq2 is to [Seq] what [iter.Seq] is to [iter.Seq2] +type Seq2[K, V any] interface { + Seq2(yield func(K, V) bool) +} + +func (sf Seq2Func[K, V]) Seq2(yield func(K, V) bool) { + sf(yield) +} + +// Pull is a wrappr around [iter.Pull] +func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func()) { + return iter.Pull(seq.Seq) +} + +// Pull2 is a wrapper around [iter.Pull2] +func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func()) { + return iter.Pull2(seq.Seq2) +} diff --git a/magic.go b/magic.go index 05a7545..971a46f 100644 --- a/magic.go +++ b/magic.go @@ -15,19 +15,20 @@ package fp import ( "cmp" + "runtime" + "slices" + "testing" ) -var GlobalErrorHandler = func(err error) { +var GlobalErrorHandler = func(err error) bool { panic(err) } -func Identity[T any](t T) T { - return t -} - func Check(err error) { if err != nil { - GlobalErrorHandler(err) + if !GlobalErrorHandler(err) { + runtime.Goexit() + } } } @@ -37,20 +38,6 @@ func Must[T any](t T, err error) T { return t } -// Errorless takes a function that can error and returns a new function that -// wraps the provided one in [fp.Must] -func Errorless[T, U any](f func(T) (U, error)) func(T) U { - return func(t T) U { - return Must(f(t)) - } -} - -func Discard[T, U any](f func(T) U) func(T) { - return func(t T) { - _ = f(t) - } -} - func Clamp[T cmp.Ordered](x T, lo T, hi T) T { return max(min(x, hi), lo) } @@ -73,3 +60,21 @@ func Ternary[T any](cond bool, a T, b T) T { } return b } + +func Assert(t *testing.T, cond bool) { + if !cond { + t.Error("Assertion failure") + } +} + +func AssertEq[T comparable](t *testing.T, a, b T) { + if a != b { + t.Errorf("Assertion failure: %+v != %+v", a, b) + } +} + +func AssertSliceEq[S ~[]E, E comparable](t *testing.T, a, b S) { + if !slices.Equal(a, b) { + t.Errorf("Assertion failure: %+v != %+v", a, b) + } +} \ No newline at end of file diff --git a/monads/monads.go b/monads/monads.go index 3c9437f..e339c54 100644 --- a/monads/monads.go +++ b/monads/monads.go @@ -1,9 +1,11 @@ package monads -import ( -) +import "github.com/rushsteve1/fp" +// Monad is a context that an operation took place in. +// You can apply additional operations to type Monad[T any] interface { + fp.Seq[T] + Ok() bool Get() (T, error) - Seq(yield func(T) bool) } diff --git a/monads/option.go b/monads/option.go index f276045..4e15a55 100644 --- a/monads/option.go +++ b/monads/option.go @@ -32,6 +32,10 @@ func None[T any]() Option[T] { } } +func (o Option[T]) Ok() bool { + return o.Valid +} + func (o Option[T]) Get() (T, error) { return o.V, fp.Ternary(o.Valid, nil, ErrUnwrapInvalid) } diff --git a/monads/result.go b/monads/result.go index 7d748ba..17be256 100644 --- a/monads/result.go +++ b/monads/result.go @@ -12,6 +12,10 @@ func Wrap[T any](v T, err error) Result[T] { } } +func (o Result[T]) Ok() bool { + return o.Err != nil +} + func (r Result[T]) Get() (T, error) { return r.V, r.Err } diff --git a/reducers/reducers.go b/reducers/reducers.go index 232e164..768beda 100644 --- a/reducers/reducers.go +++ b/reducers/reducers.go @@ -2,10 +2,9 @@ package reducers import ( "cmp" - . "iter" "slices" - "github.com/rushsteve1/fp" + . "github.com/rushsteve1/fp" "github.com/rushsteve1/fp/monads" ) @@ -16,24 +15,17 @@ type Accumulate[T, A any] func(T, A) A type Reducer[T, A any] func(Seq[T], Accumulate[T, A]) A // Collector takes a sequence and returns a single value. -// [Reducer] can be converted to Collector using [Curry] +// [Reducer] can be converted to Collector using [threading.Curry2] type Collector[T, A any] func(Seq[T]) A // Collect wraps [slices.Collect] func Collect[T any](seq Seq[T]) []T { - return slices.Collect[T](seq) -} - -// Discard consumes a sequnce, discarding the results -func Discard[T any](seq Seq[T]) { - for range seq { - // Do nothing! - } + return slices.Collect[T](seq.Seq) } // Reduce consumes a sequence returning a final accumulator value func Reduce[T, A any](seq Seq[T], a A, f Accumulate[T, A]) A { - for v := range seq { + for v := range seq.Seq { a = f(v, a) } return a @@ -60,7 +52,7 @@ func Last[T any](seq Seq[T]) (out T) { // Index returns the element at the given index, if it exists func Index[T any](seq Seq[T], i int) monads.Option[T] { ind := 0 - for v := range seq { + for v := range seq.Seq { if ind == i { return monads.Some(v) } @@ -71,7 +63,7 @@ func Index[T any](seq Seq[T], i int) monads.Option[T] { // Length returns the number of elements in a sequence func Length[T any](seq Seq[T]) (i int) { - for _ = range seq { + for _ = range seq.Seq { i++ } return i @@ -79,7 +71,7 @@ func Length[T any](seq Seq[T]) (i int) { // Max returns the maximum value of a sequence, determined by [max] func Max[T cmp.Ordered](seq Seq[T]) (out T) { - for v := range seq { + for v := range seq.Seq { out = max(out, v) } return out @@ -87,7 +79,7 @@ func Max[T cmp.Ordered](seq Seq[T]) (out T) { // Min returns the minimum value of a sequence, determined by [min] func Min[T cmp.Ordered](seq Seq[T]) (out T) { - for v := range seq { + for v := range seq.Seq { out = min(out, v) } return out @@ -96,7 +88,7 @@ func Min[T cmp.Ordered](seq Seq[T]) (out T) { func Median[T cmp.Ordered](seq Seq[T]) T { var hi T var lo T - for v := range seq { + for v := range seq.Seq { hi = max(hi, v, lo) lo = max(lo, v, hi) } @@ -105,16 +97,16 @@ func Median[T cmp.Ordered](seq Seq[T]) T { func Frequency[T cmp.Ordered](seq Seq[T]) map[T]int { out := make(map[T]int) - for v := range seq { + for v := range seq.Seq { out[v] += 1 } return out } -func Average[T fp.Numeric](seq Seq[T]) T { +func Average[T Numeric](seq Seq[T]) T { count := 0 var sum T - for v := range seq { + for v := range seq.Seq { count++ sum += v } @@ -122,7 +114,7 @@ func Average[T fp.Numeric](seq Seq[T]) T { } func Any[T any](seq Seq[T], f func(T) bool) bool { - for v := range seq { + for v := range seq.Seq { if f(v) { return true } @@ -131,7 +123,7 @@ func Any[T any](seq Seq[T], f func(T) bool) bool { } func All[T any](seq Seq[T], f func(T) bool) bool { - for v := range seq { + for v := range seq.Seq { if !f(v) { return false } diff --git a/transducers/meta.go b/transducers/meta.go index be8e2dd..3a8cb77 100644 --- a/transducers/meta.go +++ b/transducers/meta.go @@ -1,7 +1,7 @@ package transducers import ( - . "iter" + . "github.com/rushsteve1/fp" ) // Meta-transducers are functions that take or return a transducer. @@ -17,15 +17,15 @@ func Visitor[T, U any](tx Transducer[T, U]) Transducer[T, T] { // Create tne new transducer that takes the sequence return func(seq Seq[T]) Seq[T] { // Return a new sequence - return func(y1 func(T) bool) { + return SeqFunc[T](func(y1 func(T) bool) { // Create another new sequence and pass that to tx - tx(func(y2 func(T) bool) { + tx(SeqFunc[T](func(y2 func(T) bool) { // Call the passed in sequence - seq(func (t T) bool { + seq.Seq(func (t T) bool { // Yield to both new sequences return y1(t) && y2(t) }) - }) - } + })) + }) } } \ No newline at end of file diff --git a/transducers/transducers.go b/transducers/transducers.go index d434df2..e89d8b9 100644 --- a/transducers/transducers.go +++ b/transducers/transducers.go @@ -4,10 +4,9 @@ package transducers import ( "io" - . "iter" "time" - "github.com/rushsteve1/fp" + . "github.com/rushsteve1/fp" "github.com/rushsteve1/fp/reducers" ) @@ -21,29 +20,34 @@ type Transform[T, U any] func(T) U type Predicate[T any] func(T) bool // Transducer is a generalized mapping of a computation between two Sequences. -// The easiest way to create a transducer is using [magic.Curry2] on a [Transform] +// It is higher-kinded than a normal HO transform. +// Rust's iter and Elixir's Stream are transducers, but Elixir's Enum is not. +// The easiest way to create a transducer is using [threading.Curry2]. type Transducer[T, U any] func(Seq[T]) Seq[U] +// Transduce is the main event, the rest of this library exists to support it. +// It allows you to chain complex calculations into a single sequence +// and then reduce that to a single value. func Transduce[T, U, V any](tx Transducer[T, U], rx reducers.Collector[U, V], src Seq[T]) V { return rx(tx(src)) } -// Map is the simplest but shows how it all actually works the same as a transducer +// Map is the simplest, but shows how it all actually works func Map[T, U any](seq Seq[T], f Transform[T, U]) Seq[U] { - return func(yield func(U) bool) { - seq(func(t T) bool { + return SeqFunc[U](func(yield func(U) bool) { + seq.Seq(func(t T) bool { return yield(f(t)) }) - } + }) } -// Filter has the added constraint [comparable] +// Filter has the added constraint [comparable] but only needs one generic func Filter[T comparable](seq Seq[T], f Predicate[T]) Seq[T] { - return func(yield func(T) bool) { - seq(func(t T) bool { - return fp.Ternary(f(t), yield(t), true) + return SeqFunc[T](func(yield func(T) bool) { + seq.Seq(func(t T) bool { + return Ternary(f(t), yield(t), true) }) - } + }) } // Each can be trivially defined using [Map] @@ -54,7 +58,7 @@ func Each[T any](seq Seq[T], f func(T)) Seq[T] { }) } -// Take returns a new sequence that stops after count elements +// Take yields the first count elements in the sequence func Take[T any](seq Seq[T], count int) Seq[T] { i := 0 return TakeWhile(seq, func(t T) bool { @@ -66,14 +70,14 @@ func Take[T any](seq Seq[T], count int) Seq[T] { // TakeWhile yields elements while the predicate is true func TakeWhile[T any](seq Seq[T], f Predicate[T]) Seq[T] { - return func(yield func(T) bool) { - seq(func(t T) bool { + return SeqFunc[T](func(yield func(T) bool) { + seq.Seq(func(t T) bool { if f(t) { return yield(t) } return false }) - } + }) } // Drop removes the first count elements from the sequence @@ -88,9 +92,9 @@ func Drop[T any](seq Seq[T], count int) Seq[T] { // DropWhile removes elements while the predicate is true func DropWhile[T any](seq Seq[T], f Predicate[T]) Seq[T] { - return func(yield func(T) bool) { + return SeqFunc[T](func(yield func(T) bool) { dropstop := false - seq(func(t T) bool { + seq.Seq(func(t T) bool { if dropstop { return yield(t) } @@ -101,40 +105,43 @@ func DropWhile[T any](seq Seq[T], f Predicate[T]) Seq[T] { return yield(t) } }) - } + }) } // Append yields all the values of the first sequence, then the second func Append[T any](seq Seq[T], next Seq[T]) Seq[T] { - return func(yield func(T) bool) { + return SeqFunc[T](func(yield func(T) bool) { firstdone := false if !firstdone { - seq(func(t T) bool { + seq.Seq(func(t T) bool { firstdone = yield(t) return firstdone }) } else { - next(yield) + next.Seq(yield) } - } + }) } // Fuse stops the sequence at the first nil value -func Fuse[T fp.Nilable](seq Seq[T], count int) Seq[T] { - return func(yield func(T) bool) { - seq(func(t T) bool { +func Fuse[T Nilable](seq Seq[T], count int) Seq[T] { + return SeqFunc[T](func(yield func(T) bool) { + seq.Seq(func(t T) bool { if t == nil { return false } return yield(t) }) - } + }) } +// Debounce only yields values if the current element was yielded at least delay +// time since the last value was yielded. +// Elements that happen in-between debounces are dropped. func Debounce[T any](seq Seq[T], delay time.Duration) Seq[T] { - return func(yield func(T) bool) { + return SeqFunc[T](func(yield func(T) bool) { last := time.Unix(0, 0).UTC() - seq(func(t T) bool { + seq.Seq(func(t T) bool { if time.Since(last) > delay { last = time.Now().UTC() return yield(t) @@ -142,26 +149,28 @@ func Debounce[T any](seq Seq[T], delay time.Duration) Seq[T] { // Skip the debounced elements return true }) - } + }) } -func Delta[T fp.Numeric](seq Seq[T]) Seq[T] { - return func(yield func(T) bool) { +// Delta returns a new sequence that is the difference between adjacent elements +func Delta[T Numeric](seq Seq[T]) Seq[T] { + return SeqFunc[T](func(yield func(T) bool) { var prev *T - seq(func(t T) bool { + seq.Seq(func(t T) bool { if prev == nil { prev = &t return true } return yield(t - *prev) }) - } + }) } +// TimeDelta is [Delta] but specialized for [time.Time] func TimeDelta(seq Seq[time.Time]) Seq[time.Duration] { - return func(yield func(time.Duration) bool) { + return SeqFunc[time.Duration](func(yield func(time.Duration) bool) { var prev *time.Time = nil - seq(func(t time.Time) bool { + seq.Seq(func(t time.Time) bool { if prev == nil { prev = &t return true @@ -170,33 +179,37 @@ func TimeDelta(seq Seq[time.Time]) Seq[time.Duration] { prev = &t return halt }) - } + }) } +// Enumerate returns a new [Seq2] with indices as keys func Enumerate[T any](seq Seq[T]) Seq2[int, T] { - return func(yield func(int, T) bool) { + return Seq2Func[int, T](func(yield func(int, T) bool) { i := 0 - seq(func(t T) bool { + seq.Seq(func(t T) bool { stop := yield(i, t) i++ return stop }) - } + }) } +// Step only yields ever step elements func Step[T any](seq Seq[T], step int) Seq[T] { - return func(yield func(T) bool) { + return SeqFunc[T](func(yield func(T) bool) { i := 1 - seq(func(t T) bool { + seq.Seq(func(t T) bool { if i%step == 0 { return yield(t) } i++ return true }) - } + }) } +// Write will write to the given writer for every element. +// See it counterpart [generators.Reader] func Write(seq Seq[[]byte], w io.Writer) Seq[error] { return Map(seq, func(b []byte) error { _, err := w.Write(b) diff --git a/transducers/transducers_test.go b/transducers/transducers_test.go index 5ac4d7d..8ac0cd6 100644 --- a/transducers/transducers_test.go +++ b/transducers/transducers_test.go @@ -8,11 +8,10 @@ import ( "testing" "time" - . "iter" - + . "github.com/rushsteve1/fp" . "github.com/rushsteve1/fp/generators" . "github.com/rushsteve1/fp/reducers" - . "github.com/rushsteve1/fp/threading" + . "github.com/rushsteve1/fp/fun" . "github.com/rushsteve1/fp/transducers" ) @@ -26,9 +25,7 @@ func TestTransduce(t *testing.T) { Max, Integers(), ) - if s != "5" { - t.Errorf("%s != \"6\"", s) - } + AssertEq(t, s, "5") } func BenchmarkTransduce(b *testing.B) { @@ -52,14 +49,14 @@ func TestTransducerSeconds(t *testing.T) { } func TestMap(t *testing.T) { - seq := Seq[int](slices.Values([]int{1, 2, 3})) + seq := SeqFunc[int](slices.Values([]int{1, 2, 3})) tx1 := Map(seq, func(x int) int { return x * 2 }) tx2 := Map(tx1, func(x int) string { return strconv.Itoa(x) }) - sl := slices.Collect(tx2) + sl := Collect(tx2) if !slices.Equal(sl, []string{"2", "4", "6"}) { t.Fail() @@ -67,7 +64,7 @@ func TestMap(t *testing.T) { } func TestTake(t *testing.T) { - seq := slices.Values([]int{1, 2, 3, 4, 5}) + seq := SeqFunc[int](slices.Values([]int{1, 2, 3, 4, 5})) ar := Collect(Take(seq, 3)) if !slices.Equal(ar, []int{1, 2, 3}) { t.Errorf("%v is not right", ar)