diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1086c3d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: jub0bs \ No newline at end of file diff --git a/.github/workflows/iterutil.yml b/.github/workflows/iterutil.yml new file mode 100644 index 0000000..280bb89 --- /dev/null +++ b/.github/workflows/iterutil.yml @@ -0,0 +1,64 @@ +name: build + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [1.23] + steps: + - name: Check out Source + uses: actions/checkout@v4 + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + cache: true + - name: Display Go version + run: go version + - name: Test + run: go test -v -coverprofile=cover.out ./... + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: jub0bs/iterutil + benchmark: + needs: test + strategy: + matrix: + os: [ubuntu-latest] + go-version: [1.23] + name: Benchmark comparison ${{ matrix.os }} @ Go ${{ matrix.go-version }} + runs-on: ${{ matrix.os }} + steps: + - name: Check out Code (previous) + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + path: previous + - name: Check out Code (new) + uses: actions/checkout@v4 + with: + path: new + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - name: Install benchstat + run: go install golang.org/x/perf/cmd/benchstat@latest + - name: Run Benchmark (previous) + run: | + cd previous + go test -run=^$ -bench=. -count=10 . > benchmark.txt + - name: Run Benchmark (new) + run: | + cd new + go test -run=^$ -bench=. -count=10 . > benchmark.txt + - name: Run benchstat + # Mostly to compare allocations; + # measurements of execution speed in GitHub Actions are unreliable. + run: | + benchstat previous/benchmark.txt new/benchmark.txt \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d1b1d82 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] (2024-09-14) + +[0.1.0]: https://github.com/jub0bs/iterutil/releases/tag/v0.1.0 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a9b6e69 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +# Contributing to jub0bs/iterutil + +jub0bs/iterutil is an open-source project +but currently does not accept external contributions. + +However, if you want to report a problem (a bug, a missing feature, +a misfeature, an idea for performance improvement, etc.), +feel free to [open an issue](https://github.com/jub0bs/iterutil/issues/new). \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4740794 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 jub0bs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..727de9c --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# jub0bs/iterutil + +[![tag](https://img.shields.io/github/tag/jub0bs/iterutil.svg)](https://github.com/jub0bs/iterutil/releases) +![Go Version](https://img.shields.io/badge/Go-%3E%3D%201.23-%23007d9c) +[![Go Reference](https://pkg.go.dev/badge/github.com/jub0bs/iterutil.svg)](https://pkg.go.dev/github.com/jub0bs/iterutil) +[![license](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat)](https://github.com/jub0bs/iterutil/raw/main/LICENSE) +[![build](https://github.com/jub0bs/iterutil/actions/workflows/iterutil.yml/badge.svg)](https://github.com/jub0bs/iterutil/actions/workflows/iterutil.yml) +[![codecov](https://codecov.io/gh/jub0bs/iterutil/branch/main/graph/badge.svg?token=N208BHWQTM)](https://app.codecov.io/gh/jub0bs/iterutil/tree/main) +[![goreport](https://goreportcard.com/badge/jub0bs/iterutil)](https://goreportcard.com/report/jub0bs/iterutil) + +An experimental collection +(partly inspired by [Haskell][haskell]'s [prelude][prelude]) +of utility functions for working with [Go][golang] [iterators]. + +## Installation + +```shell +go get github.com/jub0bs/iterutil +``` + +jub0bs/iterutil requires Go 1.23 or above. + +## Documentation + +The documentation is available on [pkg.go.dev][pkgsite]. + +## Code coverage + +![coverage](https://codecov.io/gh/jub0bs/iterutil/branch/main/graphs/sunburst.svg?token=N208BHWQTM) + +## License + +All source code is covered by the [MIT License][license]. + +## FAQ + +### Can I depend on this library? + +You can, but at your own peril. As stated above, this library is experimental; +more than anything else, it's an excuse for me to +flex my functional-programming muscles and +familiarize myself with [Go 1.23's iterators][iterators]. +At this early stage, I reserve the right, upon new releases, to break the API: +some functions may see their names and/or signatures change, +and some functions may be removed altogether. + +If you need a few functions from this library but do not want to depend on it, +feel free to copy their sources in your project; +[a little copying is better than a little dependency][copying]. + +### How should I use this library? + +Above all, use it with parsimony. +The functional style displayed by this library is far from ideal in Go, +for several reasons: + +- code readability may suffer, in part + because Go's idiosyncracies hinder a "[fluent interface][fluent]" style and + because Go lacks a concise notation for anonymous functions; +- a more classic and imperative style is likely to prove much more performant; +- Go lacks the powerful [laziness][lazy] of [Haskell][haskell]. + +Bear in mind that the existence of this library is no license +to adopt a functional style all over your codebase! + +[copying]: https://www.youtube.com/watch?v=PAAkCSZUG1c&t=568s +[fluent]: https://en.wikipedia.org/wiki/Fluent_interface +[golang]: https://go.dev/ +[haskell]: https://www.haskell.org/ +[iterators]: https://go.dev/blog/range-functions +[lazy]: https://en.wikipedia.org/wiki/Lazy_evaluation +[license]: https://github.com/jub0bs/iterutil/blob/main/LICENSE +[pkgsite]: https://pkg.go.dev/github.com/jub0bs/iterutil +[prelude]: https://downloads.haskell.org/ghc/9.8.2/docs/libraries/base-4.19.1.0-179c/Prelude.html \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..dba3809 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,6 @@ +## Reporting security issue + +Please do **not** open an issue on GitHub. +Instead, contact jub0bs privately on [Mastodon]. + +[Mastodon]: https://infosec.exchange/@jub0bs \ No newline at end of file diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..a0ce115 --- /dev/null +++ b/doc.go @@ -0,0 +1,3 @@ +/* + */ +package iterutil diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..7a6746d --- /dev/null +++ b/errors.go @@ -0,0 +1,71 @@ +package iterutil + +import "iter" + +type ( + bwrapper interface{ Unwrap() []error } + dwrapper interface{ Unwrap() error } +) + +// AllErrors performs a preorder traversal of err and returns an iterator +// over its index-error pairs. For more context, see the [errors] package. +func AllErrors(err error) iter.Seq2[int, error] { + return func(yield func(int, error) bool) { + var i int + if !yield(i, err) { + return + } + i++ + switch err := err.(type) { + case bwrapper: + for _, err := range err.Unwrap() { + for _, err := range AllErrors(err) { + if !yield(i, err) { + return + } + i++ + } + } + case dwrapper: + for _, err := range AllErrors(err.Unwrap()) { + if !yield(i, err) { + return + } + i++ + } + default: + return + } + } +} + +// AllLeafErrors performs a preorder traversal of err and returns an iterator +// over its index-error leaf pairs. For more context, see the [errors] package. +func AllLeafErrors(err error) iter.Seq2[int, error] { + return func(yield func(int, error) bool) { + var i int + switch err := err.(type) { + case bwrapper: + for _, err := range err.Unwrap() { + for _, err := range AllLeafErrors(err) { + if !yield(i, err) { + return + } + i++ + } + } + case dwrapper: + for _, err := range AllLeafErrors(err.Unwrap()) { + if !yield(i, err) { + return + } + i++ + } + default: + if !yield(i, err) { + return + } + i++ + } + } +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 0000000..572da1e --- /dev/null +++ b/errors_test.go @@ -0,0 +1,46 @@ +package iterutil_test + +import ( + "errors" + "fmt" + + "github.com/jub0bs/iterutil" +) + +func ExampleAllErrors() { + err1 := errors.New("err1") + err2 := fmt.Errorf("err2: %w", err1) + err3 := errors.New("err3") + err4 := errors.New("err4") + err5 := errors.Join(err3, err4) + err := errors.Join(err2, err5) + for i, err := range iterutil.AllErrors(err) { + fmt.Println(i, err) + } + // Output: + // 0 err2: err1 + // err3 + // err4 + // 1 err2: err1 + // 2 err1 + // 3 err3 + // err4 + // 4 err3 + // 5 err4 +} + +func ExampleAllLeafErrors() { + err1 := errors.New("err1") + err2 := fmt.Errorf("err2: %w", err1) + err3 := errors.New("err3") + err4 := errors.New("err4") + err5 := errors.Join(err3, err4) + err := errors.Join(err2, err5) + for i, err := range iterutil.AllLeafErrors(err) { + fmt.Println(i, err) + } + // Output: + // 0 err1 + // 1 err3 + // 2 err4 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f00517a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/jub0bs/iterutil + +go 1.23.0 diff --git a/proj.go b/proj.go new file mode 100644 index 0000000..38bef91 --- /dev/null +++ b/proj.go @@ -0,0 +1,25 @@ +package iterutil + +import "iter" + +// Left return an iterator composed of the keys of the pairs in seq. +func Left[K, V any](seq iter.Seq2[K, V]) iter.Seq[K] { + return func(yield func(K) bool) { + for k := range seq { + if !yield(k) { + return + } + } + } +} + +// Right return an iterator composed of the values of the pairs in seq. +func Right[K, V any](seq iter.Seq2[K, V]) iter.Seq[V] { + return func(yield func(V) bool) { + for _, v := range seq { + if !yield(v) { + return + } + } + } +} diff --git a/proj_test.go b/proj_test.go new file mode 100644 index 0000000..e6f2191 --- /dev/null +++ b/proj_test.go @@ -0,0 +1,34 @@ +package iterutil_test + +import ( + "fmt" + "slices" + + "github.com/jub0bs/iterutil" +) + +func ExampleLeft() { + seq := slices.All([]string{"zero", "one", "two", "three", "four"}) + for i := range iterutil.Left(seq) { + fmt.Println(i) + } + // Output: + // 0 + // 1 + // 2 + // 3 + // 4 +} + +func ExampleRight() { + seq := slices.All([]string{"zero", "one", "two", "three", "four"}) + for s := range iterutil.Right(seq) { + fmt.Println(s) + } + // Output: + // zero + // one + // two + // three + // four +} diff --git a/seq.go b/seq.go new file mode 100644 index 0000000..2581edd --- /dev/null +++ b/seq.go @@ -0,0 +1,387 @@ +package iterutil + +import ( + "cmp" + "iter" +) + +// Empty returns an empty iterator. +func Empty[E any]() iter.Seq[E] { + return func(_ func(E) bool) {} +} + +// IsEmpty reports whether seq is an empty iterator. +func IsEmpty[E any](seq iter.Seq[E]) bool { + for range seq { + return false + } + return true +} + +// SeqOf returns an iterator composed of elems. +func SeqOf[E any](elems ...E) iter.Seq[E] { + return func(yield func(E) bool) { + for _, e := range elems { + if !yield(e) { + return + } + } + } +} + +// Cons returns an iterator whose head is e and whose tail is seq. +func Cons[E any](e E, seq iter.Seq[E]) iter.Seq[E] { + return func(yield func(E) bool) { + if !yield(e) { + return + } + for e := range seq { + if !yield(e) { + return + } + } + } +} + +// Head, if seq is non-empty, returns the head of seq and true; +// otherwise, it returns the zero value and false. +func Head[E any](seq iter.Seq[E]) (E, bool) { + for e := range seq { + return e, true + } + var zero E + return zero, false +} + +// Tail, if seq is non-empty, returns an iterator composed of +// all the elements of seq after the latter's head and true; +// otherwise, it returns nil and false. +func Tail[E any](seq iter.Seq[E]) (iter.Seq[E], bool) { + next, stop := iter.Pull(seq) + if _, ok := next(); !ok { + return nil, false + } + f := func(yield func(E) bool) { + defer stop() + for { + e, ok := next() + if !ok { + return + } + if !yield(e) { + return + } + } + } + return f, true +} + +// Uncons, if seq is non-empty, returns the head and tail of seq and true; +// otherwise, it returns the zero value, nil, and false. +func Uncons[E any](seq iter.Seq[E]) (E, iter.Seq[E], bool) { + next, stop := iter.Pull(seq) + head, ok := next() + if !ok { + return head, nil, false + } + tail := func(yield func(E) bool) { + defer stop() + for { + e, ok := next() + if !ok { + return + } + if !yield(e) { + return + } + } + } + return head, tail, true +} + +// Append returns an iterator resulting from the concatenation of seq1 and +// seq2. +func Append[E any](seq1, seq2 iter.Seq[E]) iter.Seq[E] { + return func(yield func(E) bool) { + for e := range seq1 { + if !yield(e) { + return + } + } + for e := range seq2 { + if !yield(e) { + return + } + } + } +} + +// Concat returns an iterator resulting from the concatenation of all iterators +// in seqs. +func Concat[E any](seqs iter.Seq[iter.Seq[E]]) iter.Seq[E] { + return func(yield func(E) bool) { + for seq := range seqs { + for e := range seq { + if !yield(e) { + return + } + } + } + } +} + +// Map returns the result of applying f to each element of seq. +func Map[A, B any](seq iter.Seq[A], f func(A) B) iter.Seq[B] { + return func(yield func(B) bool) { + for a := range seq { + if !yield(f(a)) { + return + } + } + } +} + +// Filter returns an iterator composed of the elements of seq that +// satisfy predicate p. +func Filter[E any](seq iter.Seq[E], p func(E) bool) iter.Seq[E] { + return func(yield func(E) bool) { + for e := range seq { + if !p(e) { + continue + } + if !yield(e) { + return + } + } + } +} + +// FlatMap maps f over seq and concatenates the resulting iterators. +func FlatMap[A, B any](seq iter.Seq[A], f func(A) iter.Seq[B]) iter.Seq[B] { + return func(yield func(B) bool) { + for a := range seq { + for b := range f(a) { + if !yield(b) { + return + } + } + } + } +} + +// TakeWhile returns the longest prefix of seq of elements that satisfy p. +func TakeWhile[E any](seq iter.Seq[E], p func(E) bool) iter.Seq[E] { + return func(yield func(E) bool) { + for e := range seq { + if !p(e) || !yield(e) { + return + } + } + } +} + +// DropWhile returns the suffix remaining after the longest prefix of seq +// of elements that satisfy p. +func DropWhile[E any](seq iter.Seq[E], p func(E) bool) iter.Seq[E] { + return func(yield func(E) bool) { + var doneDropping bool + for e := range seq { + if !doneDropping && p(e) { + continue + } + doneDropping = true + if !yield(e) { + return + } + } + } +} + +// Len returns the number of elements in seq. +// It terminates if and only if seq is finite. +func Len[E any](seq iter.Seq[E]) int { + var n int + for range seq { + n++ + } + return n +} + +// Take returns the prefix of seq of length count. +// If count is negative, Take returns an empty iterator. +// If count is larger than the number of elements in seq, Take returns seq. +func Take[E any](seq iter.Seq[E], count int) iter.Seq[E] { + if count < 0 { + return Empty[E]() + } + return func(yield func(E) bool) { + for e := range seq { + count-- + if count < 0 || !yield(e) { + return + } + } + } +} + +// Drop returns the suffix of seq after the first count elements. +// If count is negative, Drop returns seq. +// If count is larger than the number of elements in seq, +// Drop returns an empty iterator. +func Drop[E any](seq iter.Seq[E], count int) iter.Seq[E] { + if count < 0 { + return seq + } + return func(yield func(E) bool) { + for e := range seq { + count-- + if count >= 0 { + continue + } + if !yield(e) { + return + } + } + } +} + +// At, if seq has at least n elements, +// returns the element at index n in seq and true; +// otherwise, it returns the zero value and false. +func At[E any](seq iter.Seq[E], n int) (e E, ok bool) { + if n < 0 { + return + } + for v := range seq { + switch cmp.Compare(n, 0) { + case -1: + return + case 0: + e = v + ok = true + return + case 1: + n-- + continue + } + } + return +} + +// Contains report whether target is present in seq. +func Contains[E comparable](seq iter.Seq[E], target E) bool { + for e := range seq { + if e == target { + return true + } + } + return false +} + +// ContainsFunc reports whether at least one element e of seq satisfies p(e). +func ContainsFunc[E comparable](seq iter.Seq[E], p func(E) bool) bool { + for e := range seq { + if p(e) { + return true + } + } + return false +} + +// Foldl performs a [left-associative] [fold] of seq using +// b as the initial value and +// f as the left-associative binary operation. +// +// [fold]: https://en.wikipedia.org/wiki/Fold_(higher-order_function) +// [left-associative]: https://en.wikipedia.org/wiki/Associative_property#Notation_for_non-associative_operations +func Foldl[A, B any](seq iter.Seq[A], b B, f func(B, A) B) B { + for a := range seq { + b = f(b, a) + } + return b +} + +// ZipWith zips seq1 and seq2 with function f. +func ZipWith[A, B, C any](seq1 iter.Seq[A], seq2 iter.Seq[B], f func(A, B) C) iter.Seq[C] { + return func(yield func(C) bool) { + next1, stop1 := iter.Pull(seq1) + defer stop1() + next2, stop2 := iter.Pull(seq2) + defer stop2() + for { + a, ok1 := next1() + b, ok2 := next2() + if !ok1 || !ok2 { + return + } + if !yield(f(a, b)) { + return + } + } + } +} + +// Replicate returns an iterator of length count whose values are invariably e. +// If count is negative, Replicate returns an empty iterator. +func Replicate[E any](e E, count int) iter.Seq[E] { + if count < 0 { + return Empty[E]() + } + return func(yield func(E) bool) { + for range count { + if !yield(e) { + return + } + } + } +} + +// Repeat returns an infinite iterator whose values are invariably e. +func Repeat[E any](e E) iter.Seq[E] { + return func(yield func(E) bool) { + for { + if !yield(e) { + return + } + } + } +} + +// Iterate returns an infinite iterator composed of repeated applications +// of f to e. +func Iterate[E any](e E, f func(E) E) iter.Seq[E] { + return func(yield func(E) bool) { + for yield(e) { + e = f(e) + } + } +} + +// Cycle returns an iterator that infinitely repeats seq. +func Cycle[E any](seq iter.Seq[E]) iter.Seq[E] { + return func(yield func(E) bool) { + for { + for e := range seq { + if !yield(e) { + return + } + } + } + } +} + +// Push converts the “pull-style” iterator +// accessed by the two functions next and stop +// into a “push-style” iterator sequence. +// Push essentially is the inverse of [iter.Pull]. +func Push[E any](next func() (E, bool), stop func()) iter.Seq[E] { + return func(yield func(E) bool) { + defer stop() + for { + e, ok := next() + if !ok || !yield(e) { + return + } + } + } +} diff --git a/seq2.go b/seq2.go new file mode 100644 index 0000000..74fd676 --- /dev/null +++ b/seq2.go @@ -0,0 +1,55 @@ +package iterutil + +import "iter" + +// Len2 returns the number of elements in seq. +// It terminates if and only if seq is finite. +func Len2[K, V any](seq iter.Seq2[K, V]) int { + var n int + for range seq { + n++ + } + return n +} + +// Filter returns an iterator composed of the pairs of seq that +// satisfy predicate p. +func Filter2[L, R any](seq iter.Seq2[L, R], p func(L, R) bool) iter.Seq2[L, R] { + return func(yield func(L, R) bool) { + for l, r := range seq { + if !p(l, r) { + continue + } + if !yield(l, r) { + return + } + } + } +} + +// Swap returns an iterator over the value-key pairs of seq. +func Swap[A, B any](seq iter.Seq2[A, B]) iter.Seq2[B, A] { + return func(yield func(B, A) bool) { + for a, b := range seq { + if !yield(b, a) { + return + } + } + } +} + +// Push2 converts the “pull-style” iterator +// accessed by the two functions next and stop +// into a “push-style” iterator sequence. +// Push essentially is the inverse of [iter.Pull2]. +func Push2[K, V any](next func() (K, V, bool), stop func()) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + defer stop() + for { + k, v, ok := next() + if !ok || !yield(k, v) { + return + } + } + } +} diff --git a/seq2_test.go b/seq2_test.go new file mode 100644 index 0000000..505b565 --- /dev/null +++ b/seq2_test.go @@ -0,0 +1,55 @@ +package iterutil_test + +import ( + "fmt" + "iter" + "slices" + + "github.com/jub0bs/iterutil" +) + +func ExampleLen2() { + seq := slices.All([]int(nil)) + fmt.Println(iterutil.Len2(seq)) + seq = slices.All([]int{1, 2, 3, 4}) + fmt.Println(iterutil.Len2(seq)) + // Output: + // 0 + // 4 +} + +func ExampleFilter2() { + seq := slices.All([]string{"zero", "one", "two", "three", "four"}) + isShort := func(_ int, s string) bool { return len(s) < 5 } + for i, s := range iterutil.Filter2(seq, isShort) { + fmt.Println(i, s) + } + // Output: + // 0 zero + // 1 one + // 2 two + // 4 four +} + +func ExampleSwap() { + seq := slices.All([]string{"foo", "bar", "baz"}) + for s, i := range iterutil.Swap(seq) { + fmt.Println(s, i) + } + // Output: + // foo 0 + // bar 1 + // baz 2 +} + +func ExamplePush2() { + seq := slices.All([]string{"foo", "bar", "baz"}) + next, stop := iter.Pull2(seq) + for i, s := range iterutil.Push2(next, stop) { + fmt.Println(i, s) + } + // Output: + // 0 foo + // 1 bar + // 2 baz +} diff --git a/seq_test.go b/seq_test.go new file mode 100644 index 0000000..4908eab --- /dev/null +++ b/seq_test.go @@ -0,0 +1,330 @@ +package iterutil_test + +import ( + "fmt" + "iter" + "slices" + + "github.com/jub0bs/iterutil" +) + +func ExampleEmpty() { + for i := range iterutil.Empty[int]() { + fmt.Println(i) + } + // Output: +} + +func ExampleIsEmpty() { + seq := slices.Values([]int{}) + fmt.Println(iterutil.IsEmpty(seq)) + seq = slices.Values([]int{1, 2, 3, 4}) + fmt.Println(iterutil.IsEmpty(seq)) + // Output: + // true + // false +} + +func ExampleSeqOf() { + for i := range iterutil.SeqOf(1, 2, 3) { + fmt.Println(i) + } + // Output: + // 1 + // 2 + // 3 +} + +func ExampleCons() { + seq := iterutil.Cons(0, slices.Values([]int{1, 2, 3})) + for i := range seq { + fmt.Println(i) + } + // Output: + // 0 + // 1 + // 2 + // 3 +} + +func ExampleHead() { + seq := slices.Values([]int{}) + fmt.Println(iterutil.Head(seq)) + seq = slices.Values([]int{1, 2, 3, 4}) + fmt.Println(iterutil.Head(seq)) + // Output: + // 0 false + // 1 true +} + +func ExampleTail() { + seq := slices.Values([]int{}) + tail, ok := iterutil.Tail(seq) + if ok { + fmt.Println(slices.Collect(tail)) + } + seq = slices.Values([]int{1, 2, 3, 4}) + tail, ok = iterutil.Tail(seq) + if ok { + fmt.Println(slices.Collect(tail)) + } + // Output: [2 3 4] +} + +func ExampleUncons() { + seq := slices.Values([]int{}) + head, tail, ok := iterutil.Uncons(seq) + if ok { + fmt.Println(head, slices.Collect(tail)) + } + seq = slices.Values([]int{1, 2, 3, 4}) + head, tail, ok = iterutil.Uncons(seq) + if ok { + fmt.Println(head, slices.Collect(tail)) + } + // Output: 1 [2 3 4] +} + +func ExampleAppend() { + seq1 := slices.Values([]string{"foo", "bar"}) + seq2 := slices.Values([]string{"baz", "qux"}) + for s := range iterutil.Append(seq1, seq2) { + fmt.Println(s) + } + // Output: + // foo + // bar + // baz + // qux +} + +func ExampleConcat() { + seq1 := slices.Values([]string{"foo", "bar"}) + seq2 := slices.Values([]string{"baz", "qux"}) + seqs := slices.Values([]iter.Seq[string]{seq1, seq2}) + for s := range iterutil.Concat(seqs) { + fmt.Println(s) + } + // Output: + // foo + // bar + // baz + // qux +} + +func ExampleMap() { + seq := slices.Values([]string{"one", "two", "three"}) + length := func(s string) int { return len(s) } + for s := range iterutil.Map(seq, length) { + fmt.Println(s) + } + // Output: + // 3 + // 3 + // 5 +} + +func ExampleFilter() { + seq := slices.Values([]int{1, 42, 99, 100}) + isOdd := func(i int) bool { return i%2 != 0 } + for s := range iterutil.Filter(seq, isOdd) { + fmt.Println(s) + } + // Output: + // 1 + // 99 +} + +func ExampleFlatMap() { + seq := slices.Values([]int{0, 1, 2, 3}) + repeatN := func(i int) iter.Seq[int] { + return slices.Values(slices.Repeat([]int{i}, i)) + } + for i := range iterutil.FlatMap(seq, repeatN) { + fmt.Println(i) + } + // Output: + // 1 + // 2 + // 2 + // 3 + // 3 + // 3 +} + +func ExampleTakeWhile() { + seq := slices.Values([]string{"foo", "bar", "baz", "qux"}) + isNotBaz := func(s string) bool { return s != "baz" } + for s := range iterutil.TakeWhile(seq, isNotBaz) { + fmt.Println(s) + } + // Output: + // foo + // bar +} + +func ExampleDropWhile() { + seq := slices.Values([]string{"foo", "bar", "baz", "qux"}) + isNotBaz := func(s string) bool { return s != "baz" } + for s := range iterutil.DropWhile(seq, isNotBaz) { + fmt.Println(s) + } + // Output: + // baz + // qux +} + +func ExampleLen() { + seq := slices.Values([]int{}) + fmt.Println(iterutil.Len(seq)) + seq = slices.Values([]int{1, 2, 3, 4}) + fmt.Println(iterutil.Len(seq)) + // Output: + // 0 + // 4 +} + +func ExampleTake() { + seq := slices.Values([]string{"foo", "bar", "baz", "qux"}) + for s := range iterutil.Take(seq, 2) { + fmt.Println(s) + } + // Output: + // foo + // bar +} + +func ExampleDrop() { + seq := slices.Values([]string{"foo", "bar", "baz", "qux"}) + for s := range iterutil.Drop(seq, 3) { + fmt.Println(s) + } + // Output: + // qux +} + +func ExampleAt() { + seq := slices.Values([]string{"foo", "bar", "baz", "qux"}) + fmt.Println(iterutil.At(seq, 2)) + // Output: + // baz true +} + +func ExampleContains() { + seq := slices.Values([]int{1}) + fmt.Println(iterutil.Contains(seq, 2)) + seq = slices.Values([]int{1, 2, 3}) + fmt.Println(iterutil.Contains(seq, 2)) + // Output: + // false + // true +} + +func ExampleContainsFunc() { + isEven := func(i int) bool { return i%2 == 0 } + seq := slices.Values([]int{1}) + fmt.Println(iterutil.ContainsFunc(seq, isEven)) + seq = slices.Values([]int{1, 2, 3}) + fmt.Println(iterutil.ContainsFunc(seq, isEven)) + // Output: + // false + // true +} + +func ExampleFoldl() { + seq := slices.Values([]int{1, 2, 3, 4, 5, 6}) + plus := func(i, j int) int { return i + j } + sum := iterutil.Foldl(seq, 0, plus) + fmt.Println(sum) + // Output: 21 +} + +func ExampleZipWith() { + french := slices.Values([]string{"un", "deux", "trois", "quatre", "cinq"}) + english := slices.Values([]string{"one", "two", "three", "four"}) + join := func(fr, en string) string { return fr + " => " + en } + seq := iterutil.ZipWith(french, english, join) + for s := range seq { + fmt.Println(s) + } + // Output: + // un => one + // deux => two + // trois => three + // quatre => four +} + +func ExampleReplicate() { + for s := range iterutil.Replicate("foo", 4) { + fmt.Println(s) + } + // Output: + // foo + // foo + // foo + // foo +} + +func ExampleRepeat() { + seq := iterutil.Repeat(42) + var count int + for i := range seq { + count++ + if count > 3 { + break + } + fmt.Println(i) + } + // Output: + // 42 + // 42 + // 42 +} + +func ExampleIterate() { + double := func(i int) int { return i + i } + seq := iterutil.Iterate(1, double) + for i := range seq { + if i > 20 { + break + } + fmt.Println(i) + } + // Output: + // 1 + // 2 + // 4 + // 8 + // 16 +} + +func ExampleCycle() { + seq := slices.Values([]int{1, 2, 3}) + cycle := iterutil.Cycle(seq) + var count int + for i := range cycle { + count++ + if count > 5 { + break + } + fmt.Println(i) + } + // Output: + // 1 + // 2 + // 3 + // 1 + // 2 +} + +func ExamplePush() { + seq := slices.Values([]int{1, 2, 3}) + next, stop := iter.Pull(seq) + for i := range iterutil.Push(next, stop) { + fmt.Println(i) + } + // Output: + // 1 + // 2 + // 3 +} diff --git a/zip.go b/zip.go new file mode 100644 index 0000000..7675689 --- /dev/null +++ b/zip.go @@ -0,0 +1,23 @@ +package iterutil + +import "iter" + +// Zip zips seq1 and seq2 into a sequence of corresponding pairs. +func Zip[K, V any](seq1 iter.Seq[K], seq2 iter.Seq[V]) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + next1, stop1 := iter.Pull(seq1) + defer stop1() + next2, stop2 := iter.Pull(seq2) + defer stop2() + for { + k, ok1 := next1() + v, ok2 := next2() + if !ok1 || !ok2 { + return + } + if !yield(k, v) { + return + } + } + } +} diff --git a/zip_test.go b/zip_test.go new file mode 100644 index 0000000..6eadac3 --- /dev/null +++ b/zip_test.go @@ -0,0 +1,21 @@ +package iterutil_test + +import ( + "fmt" + "slices" + + "github.com/jub0bs/iterutil" +) + +func ExampleZip() { + french := slices.Values([]string{"un", "deux", "trois", "quatre", "cinq"}) + english := slices.Values([]string{"one", "two", "three"}) + seq := iterutil.Zip(french, english) + for f, e := range seq { + fmt.Println(f, "=>", e) + } + // Output: + // un => one + // deux => two + // trois => three +}