From 7932f1bd5418db5e3465cd060c6367447b4a23b0 Mon Sep 17 00:00:00 2001 From: jub0bs Date: Sat, 14 Sep 2024 20:20:39 +0200 Subject: [PATCH] initial commit --- .github/FUNDING.yml | 1 + .github/workflows/iterutil.yml | 64 ++++++ CHANGELOG.md | 10 + CONTRIBUTING.md | 8 + LICENSE | 21 ++ README.md | 74 +++++++ SECURITY.md | 6 + doc.go | 3 + errors.go | 71 ++++++ errors_test.go | 46 ++++ go.mod | 3 + proj.go | 25 +++ proj_test.go | 34 +++ seq.go | 387 +++++++++++++++++++++++++++++++++ seq2.go | 55 +++++ seq2_test.go | 55 +++++ seq_test.go | 330 ++++++++++++++++++++++++++++ zip.go | 23 ++ zip_test.go | 21 ++ 19 files changed, 1237 insertions(+) create mode 100644 .github/FUNDING.yml create mode 100644 .github/workflows/iterutil.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 doc.go create mode 100644 errors.go create mode 100644 errors_test.go create mode 100644 go.mod create mode 100644 proj.go create mode 100644 proj_test.go create mode 100644 seq.go create mode 100644 seq2.go create mode 100644 seq2_test.go create mode 100644 seq_test.go create mode 100644 zip.go create mode 100644 zip_test.go 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 +}