Skip to content

Commit

Permalink
Adapters for Go 1.22 range-over-func, refinements to iterator and num…
Browse files Browse the repository at this point in the history
…eric ranging.
  • Loading branch information
jpfourny committed Jan 21, 2024
1 parent 267584e commit 47ca46f
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 155 deletions.
6 changes: 3 additions & 3 deletions examples/stream/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ func TestMapPairReverse(t *testing.T) {
// ("bar", 2)
}

func TestMapNumberToBool(t *testing.T) {
func TestMapNumToBool(t *testing.T) {
// Given stream of numbers, map each number to whether it is even.
s := stream.Map(
stream.Of(0, 2, 0),
mapper2.NumberToBool[int, bool](),
mapper2.NumToBool[int, bool](),
)
stream.ForEach(s, func(b bool) {
fmt.Println(b)
Expand All @@ -122,7 +122,7 @@ func TestMapBoolNumber(t *testing.T) {
// Given stream of bools, map each bool to 0 if false and 1 if true.
s := stream.Map(
stream.Of(false, true, false),
mapper2.BoolToNumber[bool, int](),
mapper2.BoolToNum[bool, int](),
)
stream.ForEach(s, func(i int) {
fmt.Println(i)
Expand Down
30 changes: 14 additions & 16 deletions pkg/pair/pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,33 @@ import (
"fmt"
)

// Pair represents a pair of two values.
// It is a generic type that takes two type parameters A and B.
// Zero returns a new Pair with default zero values for the type parameters A and B.
func Zero[A, B any]() Pair[A, B] {
return Pair[A, B]{}
}

// Of creates a new Pair with the provided values for the first and second elements.
func Of[A, B any](first A, second B) Pair[A, B] {
return Pair[A, B]{first: first, second: second}
}

// Pair represents a pair of values of type A and B.
type Pair[A, B any] struct {
first A
second B
}

// First returns the first element of a Pair.
// First returns the first element of type A.
func (p Pair[A, B]) First() A {
return p.first
}

// Second returns the second element of a Pair.
// Second returns the second element of type B.
func (p Pair[A, B]) Second() B {
return p.second
}

// Explode returns the first and second elements of a Pair.
// Explode returns the first and second elements together as a tuple.
func (p Pair[A, B]) Explode() (A, B) {
return p.first, p.second
}
Expand All @@ -35,14 +44,3 @@ func (p Pair[A, B]) Reverse() Pair[B, A] {
func (p Pair[A, B]) String() string {
return fmt.Sprintf("(%#v, %#v)", p.first, p.second)
}

// Zero returns a new Pair with default zero values for the type parameters A and B.
func Zero[A, B any]() Pair[A, B] {
return Pair[A, B]{}
}

// Of creates a new Pair with the provided values for the first and second elements.
// The type parameters A and B determine the types of the first and second elements, respectively.
func Of[A, B any](first A, second B) Pair[A, B] {
return Pair[A, B]{first: first, second: second}
}
2 changes: 1 addition & 1 deletion pkg/stream/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func Reduce[E any](s Stream[E], reduce Reducer[E]) optional.Optional[E] {
// n2 := stream.Sum[float64](stream.Of(1, 2, 3)) // 6.0 (float64)
func Sum[R, E constraint.RealNumber](s Stream[E]) R {
return Reduce(
Map(s, mapper.NumberToNumber[E, R]()),
Map(s, mapper.NumToNum[E, R]()),
reducer.Sum[R](),
).OrElseZero()
}
Expand Down
33 changes: 2 additions & 31 deletions pkg/stream/combine.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ func CombineOrDiscard[E1, E2, F any](s1 Stream[E1], s2 Stream[E2], combine Optio
// s := stream.Zip(stream.Of(1, 2, 3), stream.Of("foo", "bar"))
// out := stream.DebugString(s) // "<(1, foo), (2, bar)>"
func Zip[E, F any](s1 Stream[E], s2 Stream[F]) Stream[pair.Pair[E, F]] {
return Combine(s1, s2, func(e E, f F) pair.Pair[E, F] {
return pair.Of(e, f)
})
return Combine(s1, s2, pair.Of[E, F])
}

// ZipWithIndex returns a stream that pairs each element in the input stream with its index, starting at the given offset.
Expand All @@ -120,32 +118,5 @@ func Zip[E, F any](s1 Stream[E], s2 Stream[F]) Stream[pair.Pair[E, F]] {
// s := stream.ZipWithIndex(stream.Of("foo", "bar"), 1)
// out := stream.DebugString(s) // "<(foo, 1), (bar, 2)>"
func ZipWithIndex[E any, I constraint.Integer](s Stream[E], offset I) Stream[pair.Pair[E, I]] {
return Combine(
s,
Range[I](offset, pred.True[I](), mapper.Increment[I](1)),
func(e E, i I) pair.Pair[E, I] {
return pair.Of(e, i)
},
)
}

// KeyExtractor represents a function that extracts a key of type K from a value of type E.
type KeyExtractor[E, K any] func(e E) K

// ZipWithKey returns a stream that pairs each element in the input stream with the key extracted from the element using the given key extractor.
// The resulting stream will have the same number of elements as the input stream.
//
// Example usage:
//
// s := stream.ZipWithKey(
// stream.Of("foo", "bar"),
// func(s string) string {
// return strings.ToUpper(s)
// },
// )
// out := stream.DebugString(s) // "<(FOO, foo), (BAR, bar)>"
func ZipWithKey[K, E any](s Stream[E], ke KeyExtractor[E, K]) Stream[pair.Pair[K, E]] {
return Map(s, func(e E) pair.Pair[K, E] {
return pair.Of(ke(e), e)
})
return Zip(s, Walk(offset, pred.True[I](), mapper.Increment[I](1)))
}
36 changes: 0 additions & 36 deletions pkg/stream/combine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,39 +153,3 @@ func TestZipWithIndex(t *testing.T) {
assert.ElementsMatch(t, got, want)
})
}

func TestZipWithKey(t *testing.T) {
t.Run("empty", func(t *testing.T) {
s := ZipWithKey(Empty[int](), func(e int) int {
return e * 10
})
got := CollectSlice(s)
var want []pair.Pair[int, int]
assert.ElementsMatch(t, got, want)
})

t.Run("non-empty", func(t *testing.T) {
s := ZipWithKey(Of(1, 2, 3), func(e int) int {
return e * 10
})
got := CollectSlice(s)
want := []pair.Pair[int, int]{
pair.Of(10, 1),
pair.Of(20, 2),
pair.Of(30, 3),
}
assert.ElementsMatch(t, got, want)
})

t.Run("limited", func(t *testing.T) {
s := ZipWithKey(Of(1, 2, 3), func(e int) int {
return e * 10
})
got := CollectSlice(Limit(s, 2)) // Stops stream after 2 elements.
want := []pair.Pair[int, int]{
pair.Of(10, 1),
pair.Of(20, 2),
}
assert.ElementsMatch(t, got, want)
})
}
26 changes: 24 additions & 2 deletions pkg/stream/generate.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package stream

import "math/rand"
import (
"math/rand"
)

// Generator represents a function that produces an infinite sequence of elements of type E.
// Used by the Generate function.
// Example: Random numbers.
type Generator[E any] func() E

// Generate returns an infinite stream that produces elements by invoking the given generator function.
Expand All @@ -23,6 +24,27 @@ func Generate[E any](next Generator[E]) Stream[E] {
}
}

// Repeat returns an infinite stream that always produces the given element.
//
// Example usage:
//
// s := stream.Repeat(1)
// out := stream.DebugString(s) // "<1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...>"
func Repeat[E any](e E) Stream[E] {
return Generate(func() E { return e })
}

// RepeatN returns a stream that produces the given element n times.
// If n is less than or equal to zero, the stream will be empty.
//
// Example usage:
//
// s := stream.RepeatN(1, 3)
// out := stream.DebugString(s) // "<1, 1, 1>"
func RepeatN[E any](e E, n int64) Stream[E] {
return Limit(Repeat(e), n)
}

// RandomInt returns a stream that produces pseudo-random, non-negative int values using the given rand.Source.
func RandomInt(source rand.Source) Stream[int] {
rnd := rand.New(source)
Expand Down
12 changes: 12 additions & 0 deletions pkg/stream/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ func TestGenerate(t *testing.T) {
assert.ElementsMatch(t, out, []int{1, 1, 1, 1, 1})
}

func TestRepeat(t *testing.T) {
s := Repeat(1)
out := CollectSlice(Limit(s, 5)) // Limit to 5 elements for testing.
assert.ElementsMatch(t, out, []int{1, 1, 1, 1, 1})
}

func TestRepeatN(t *testing.T) {
s := RepeatN(1, 3)
out := CollectSlice(s)
assert.ElementsMatch(t, out, []int{1, 1, 1})
}

func TestRandomInt(t *testing.T) {
s := RandomInt(rand.NewSource(0)) // Seed with 0 for testing - should produce the same sequence every time.
out := CollectSlice[int](Limit(s, 5)) // Limit to 5 elements for testing.
Expand Down
79 changes: 60 additions & 19 deletions pkg/stream/iterate.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
package stream

import "github.com/jpfourny/papaya/pkg/optional"
import (
"github.com/jpfourny/papaya/pkg/constraint"
"github.com/jpfourny/papaya/pkg/optional"
"github.com/jpfourny/papaya/pkg/stream/mapper"
"github.com/jpfourny/papaya/pkg/stream/pred"
)

// Iterator represents a function that produces a (typically finite) sequence of elements of type E.
// It is similar to Generator, but it can return an empty optional.Optional to signal the end of the sequence.
// Used by the Iterate function.
type Iterator[E any] func() optional.Optional[E]
// Iterate returns a stream of elements produced by the given iterator function.
// When the iterator function returns false, the stream ends.
//
// Example usage:
//
// i := 0
// s := stream.Iterate(func() (int, bool) {
// if i < 3 {
// i++
// return i, true
// }
// return 0, false
// })
// out := stream.DebugString(s) // "<1, 2, 3>"
func Iterate[E any](next func() (E, bool)) Stream[E] {
return func(yield Consumer[E]) bool {
for e, ok := next(); ok; e, ok = next() {
if !yield(e) {
return false
}
}
return true
}
}

// Iterate returns a stream of elements produced by the given Iterator.
// When the Iterator returns an empty optional.Optional, the stream ends.
// IterateOptional returns a stream of elements produced by the given iterator function.
// When the iterator function returns an empty optional.Optional, the stream ends.
//
// Example usage:
//
// i := 0
// s := stream.Iterate(func() optional.Optional[int] {
// s := stream.IterateOptional(func() optional.Optional[int] {
// if i < 3 {
// i++
// return optional.Of(i)
// }
// return optional.Empty[int]()
// })
// out := stream.DebugString(s) // "<1, 2, 3>"
func Iterate[E any](next Iterator[E]) Stream[E] {
func IterateOptional[E any](next func() optional.Optional[E]) Stream[E] {
return func(yield Consumer[E]) bool {
for e := next(); e.Present(); e = next() {
if !yield(e.Get()) {
Expand All @@ -32,21 +57,37 @@ func Iterate[E any](next Iterator[E]) Stream[E] {
}
}

// Range returns a stream that produces elements over a range beginning at `start`, advanced by the `next` function, and ending when `cond` predicate returns false.
// Walk returns a stream that walks elements beginning at `start`, advanced by the `advance` function, and ending when `cond` predicate returns false.
//
// Example usage:
//
// s := stream.Range(1, pred.LessThanOrEqual(5), mapper.Increment(2))
// s := stream.Walk(1, pred.LessThanOrEqual(5), mapper.Increment(2))
// out := stream.DebugString(s) // "<1, 3, 5>"
func Range[E any](start E, cond Predicate[E], next Mapper[E, E]) Stream[E] {
e := start
return Iterate(func() (result optional.Optional[E]) {
if cond(e) {
result = optional.Of(e)
e = next(e)
} else {
result = optional.Empty[E]()
func Walk[E any](start E, cond Predicate[E], advance Mapper[E, E]) Stream[E] {
next := start
return Iterate(func() (e E, ok bool) {
if cond(next) {
e, ok = next, true
next = advance(next)
}
return
})
}

// Interval returns a stream of real-number type N from the half-open interval `[start, end)` using the given step size.
// If the step size is negative, then the stream will be decreasing; otherwise, it will be increasing.
//
// Example usage:
//
// s := stream.Interval(0, 5, 1)
// out := stream.DebugString(s) // "<0, 1, 2, 3, 4>"
// s := stream.Interval(0, 5, 2)
// out := stream.DebugString(s) // "<0, 2, 4>"
// s = stream.Interval(5, 0, -2)
// out = stream.DebugString(s) // "<5, 3, 1>"
func Interval[N constraint.RealNumber](start, end, step N) Stream[N] {
if step < 0 {
return Walk[N](start, pred.GreaterThan(end), mapper.Increment(step))
}
return Walk[N](start, pred.LessThan(end), mapper.Increment(step))
}
Loading

0 comments on commit 47ca46f

Please sign in to comment.