├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ └── test.yaml
├── .gitignore
├── .golangci.yml
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── assets
├── banner_dark.png
└── banner_light.png
├── go.mod
├── internal
├── assert
│ └── assert.go
└── fakes
│ ├── fakes.go
│ └── reader.go
└── it
├── chain.go
├── chain_test.go
├── channel.go
├── channel_test.go
├── compact.go
├── compact_test.go
├── cycle.go
├── cycle_test.go
├── drop.go
├── drop_test.go
├── enumerate.go
├── enumerate_test.go
├── exhausted.go
├── exhausted_test.go
├── filter.go
├── filter
├── filter.go
└── filter_test.go
├── filter_test.go
├── filter_unique.go
├── filter_unique_test.go
├── integers.go
├── integers_test.go
├── iter.go
├── iter_test.go
├── itx
├── chain.go
├── chain_test.go
├── channel.go
├── channel_test.go
├── cycle.go
├── cycle_test.go
├── drop.go
├── drop_test.go
├── enumerate.go
├── enumerate_test.go
├── exhausted.go
├── exhausted_test.go
├── filter.go
├── filter_test.go
├── integers.go
├── integers_test.go
├── iter.go
├── iter_test.go
├── lines.go
├── lines_test.go
├── map.go
├── map_test.go
├── once.go
├── once_test.go
├── repeat.go
├── repeat_test.go
├── take.go
├── take_test.go
├── zip.go
└── zip_test.go
├── lines.go
├── lines_test.go
├── map.go
├── map_test.go
├── once.go
├── once_test.go
├── op
├── op.go
└── op_test.go
├── repeat.go
├── repeat_test.go
├── take.go
├── take_test.go
├── zip.go
└── zip_test.go
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: '[Bug 🐛]: '
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **What happened?**
11 |
12 | _Provide a description of the issue._
13 |
14 | **What did you expect to happen?**
15 |
16 | _Explain what you would have expected to happen instead._
17 |
18 | **Which version of go-functional were you using?**
19 |
20 | _Provide the version number (such as `v0.3.0`)._
21 |
22 | **To Reproduce**
23 |
24 | _Provide a code snippet or steps to reproduce the issue._
25 |
26 | **Do you intend to fix this issue yourself?**
27 |
28 | _Yes or no. This is so that the maintainers know to leave you time to make a
29 | contribution rather than just fixing it themselves._
30 |
31 | **Additional context**
32 |
33 | _Add any other context about the problem here._
34 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: '[Feature 🔨]: '
5 | labels: 'enhancement'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 |
12 | A clear and concise description of what the problem is. Ex. I'm always
13 | frustrated when [...].
14 |
15 | **Describe the solution you'd like**
16 |
17 | A clear and concise description of what you want to happen.
18 |
19 | **Provide code snippets to show how this new feature might be used.**
20 |
21 | Ideally, provide an [`Example`](https://go.dev/blog/examples) test that
22 | demonstrates the feature's usage.
23 |
24 | **Does this incur a breaking change?**
25 |
26 | Yes / no.
27 |
28 | **Do you intend to build this feature yourself?**
29 |
30 | Yes / no (so that it's clear that maintainers should allow you to make a
31 | contribution).
32 |
33 | **Additional context**
34 | Add any other context or screenshots about the feature request here.
35 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: /
5 | schedule: { interval: weekly }
6 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | **Please provide a brief description of the change.**
2 |
3 | A sentence or two is fine, the rest should be clear from the code change and related issue.
4 |
5 | **Which issue does this change relate to?**
6 |
7 | Please provide a link to the issue that this change resolved.
8 |
9 | If there is no such issue, consider creating one first. Discussions concerning proposed changes ought to take place in an issue and not in pull requests. Pull requests not associated with an issue are less likely to be merged and more likely to ask for changes.
10 |
11 | **Contribution checklist.**
12 |
13 | _Replace the space in each box with "X" to check it off._
14 |
15 | - [ ] I have read and understood the CONTRIBUTING guidelines
16 | - [ ] All commits in my PR conform to the commit hygiene section
17 | - [ ] I have added relevant tests
18 | - [ ] I have not added any dependencies
19 |
20 | **Additional context**
21 |
22 | _Add any other context about the problem here._
23 |
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push: { branches: [main] }
4 | pull_request: { branches: [main] }
5 | jobs:
6 | check:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v5
10 | - uses: actions/setup-go@v6
11 | with:
12 | go-version: "1.25"
13 | cache: false
14 | - run: make check
15 | env: { SKIP_LINT: true }
16 | - uses: golangci/golangci-lint-action@v8
17 | with: { version: "latest" }
18 | - run: make cov
19 | - uses: codecov/codecov-action@v5
20 | env: { CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" }
21 | test:
22 | strategy:
23 | matrix:
24 | os: [ubuntu-latest, windows-latest, macos-latest]
25 | go-version: ["1.23", "1.24", "1.25"]
26 | runs-on: ${{ matrix.os }}
27 | steps:
28 | - uses: actions/checkout@v5
29 | - uses: actions/setup-go@v6
30 | with:
31 | go-version: ${{ matrix.go-version }}
32 | cache: false
33 | - run: make test
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage.txt
2 | .vscode/
3 | go.work
4 | .envrc
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | default: none
4 | enable:
5 | - asasalint
6 | - asciicheck
7 | - bidichk
8 | - decorder
9 | - depguard
10 | - durationcheck
11 | - errcheck
12 | - errchkjson
13 | - errorlint
14 | - exhaustive
15 | - gochecknoglobals
16 | - gocritic
17 | - gocyclo
18 | - gosec
19 | - govet
20 | - ineffassign
21 | - misspell
22 | - nilerr
23 | - staticcheck
24 | - unused
25 | settings:
26 | cyclop:
27 | max-complexity: 15
28 | package-average: 0.5
29 | depguard:
30 | rules:
31 | main:
32 | allow:
33 | - $gostd
34 | - github.com/BooleanCat/go-functional/v2
35 | govet:
36 | enable-all: true
37 | misspell:
38 | locale: US
39 | exclusions:
40 | generated: lax
41 | presets:
42 | - comments
43 | - common-false-positives
44 | - legacy
45 | - std-error-handling
46 | paths:
47 | - third_party$
48 | - builtin$
49 | - examples$
50 | formatters:
51 | enable:
52 | - gofmt
53 | exclusions:
54 | generated: lax
55 | paths:
56 | - third_party$
57 | - builtin$
58 | - examples$
59 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "proseWrap": "always",
5 | "printWidth": 100
6 | }
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a
6 | harassment-free experience for everyone, regardless of age, body size, visible or invisible
7 | disability, ethnicity, sex characteristics, gender identity and expression, level of experience,
8 | education, socio-economic status, nationality, personal appearance, race, religion, or sexual
9 | identity and orientation.
10 |
11 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and
12 | healthy community.
13 |
14 | ## Our Standards
15 |
16 | Examples of behaviour that contributes to a positive environment for our community include:
17 |
18 | - Demonstrating empathy and kindness toward other people
19 | - Being respectful of differing opinions, viewpoints, and experiences
20 | - Giving and gracefully accepting constructive feedback
21 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the
22 | experience
23 | - Focusing on what is best not just for us as individuals, but for the overall community
24 |
25 | Examples of unacceptable behaviour include:
26 |
27 | - The use of sexualized language or imagery, and sexual attention or advances of any kind
28 | - Trolling, insulting or derogatory comments, and personal or political attacks
29 | - Public or private harassment
30 | - Publishing others' private information, such as a physical or email address, without their
31 | explicit permission
32 | - Other conduct which could reasonably be considered inappropriate in a professional setting
33 |
34 | ## Enforcement Responsibilities
35 |
36 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behaviour
37 | and will take appropriate and fair corrective action in response to any behaviour that they deem
38 | inappropriate, threatening, offensive, or harmful.
39 |
40 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits,
41 | code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and
42 | will communicate reasons for moderation decisions when appropriate.
43 |
44 | ## Scope
45 |
46 | This Code of Conduct applies within all community spaces, and also applies when an individual is
47 | officially representing the community in public spaces. Examples of representing our community
48 | include using an official e-mail address, posting via an official social media account, or acting as
49 | an appointed representative at an online or offline event.
50 |
51 | ## Enforcement
52 |
53 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be reported to the
54 | community leaders responsible for enforcement at tomgodkin@pm.me. All complaints will be reviewed
55 | and investigated promptly and fairly.
56 |
57 | All community leaders are obligated to respect the privacy and security of the reporter of any
58 | incident.
59 |
60 | ## Enforcement Guidelines
61 |
62 | Community leaders will follow these Community Impact Guidelines in determining the consequences for
63 | any action they deem in violation of this Code of Conduct:
64 |
65 | ### 1. Correction
66 |
67 | **Community Impact**: Use of inappropriate language or other behaviour deemed unprofessional or
68 | unwelcome in the community.
69 |
70 | **Consequence**: A private, written warning from community leaders, providing clarity around the
71 | nature of the violation and an explanation of why the behaviour was inappropriate. A public apology
72 | may be requested.
73 |
74 | ### 2. Warning
75 |
76 | **Community Impact**: A violation through a single incident or series of actions.
77 |
78 | **Consequence**: A warning with consequences for continued behaviour. No interaction with the people
79 | involved, including unsolicited interaction with those enforcing the Code of Conduct, for a
80 | specified period of time. This includes avoiding interactions in community spaces as well as
81 | external channels like social media. Violating these terms may lead to a temporary or permanent ban.
82 |
83 | ### 3. Temporary Ban
84 |
85 | **Community Impact**: A serious violation of community standards, including sustained inappropriate
86 | behaviour.
87 |
88 | **Consequence**: A temporary ban from any sort of interaction or public communication with the
89 | community for a specified period of time. No public or private interaction with the people involved,
90 | including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this
91 | period. Violating these terms may lead to a permanent ban.
92 |
93 | ### 4. Permanent Ban
94 |
95 | **Community Impact**: Demonstrating a pattern of violation of community standards, including
96 | sustained inappropriate behaviour, harassment of an individual, or aggression toward or
97 | disparagement of classes of individuals.
98 |
99 | **Consequence**: A permanent ban from any sort of public interaction within the community.
100 |
101 | ## Attribution
102 |
103 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at
104 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
105 |
106 | Community Impact Guidelines were inspired by
107 | [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
108 |
109 | [homepage]: https://www.contributor-covenant.org
110 |
111 | For answers to common questions about this code of conduct, see the FAQ at
112 | https://www.contributor-covenant.org/faq. Translations are available at
113 | https://www.contributor-covenant.org/translations.
114 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to go-functional
2 |
3 | Contributions to this project are very welcome! This guide should help with instructions on how to
4 | submit changes. Contributions can be made in the form of GitHub
5 | [issues](https://github.com/BooleanCat/go-functional/issues) or
6 | [pull requests](https://github.com/BooleanCat/go-functional/pulls).
7 |
8 | When submitting an issue, please choose the relevant template or choose a blank issue if your query
9 | doesn't naturally fit into an existing template. The pull request template contains a contribution
10 | checklist.
11 |
12 | ## Zero-dependency
13 |
14 | This project is a zero-dependency project - which means that consumers using this project's packages
15 | must only incur one dependency: go-functional.
16 |
17 | Development dependencies are OK as they will not be included as dependencies to end-users (such as
18 | `golangci-lint`).
19 |
20 | ## Development dependencies
21 |
22 | 1. [golangci-lint](https://github.com/golangci/golangci-lint) is used to lint the project when
23 | running `make check`.
24 | 2. [counterfeiter](https://github.com/maxbrunsfeld/counterfeiter) is used to generate new fakes
25 | (using `go generate ./...`). Fakes are declared in `internal/fakes/fakes.go`.
26 |
27 | ## Commit hygiene
28 |
29 | - Commits should contain only a single change
30 | - Commit messages must use imperative language (e.g. `Add iter.Fold collection function`)
31 | - Commit messages must explain what is changed, not how it is changed
32 | - The first line of a commit message should be a terse description of the change containing 72
33 | characters or fewer
34 |
35 | ## Running tests
36 |
37 | Run tests with `make test` from the project root directory.
38 |
39 | Tests are written using Go's `testing` package and helpers are available in `internal/assert`.
40 |
41 | Code is linted using `golangci-lint`. The linter may be run using `make lint`.
42 |
43 | ## Different types of changes
44 |
45 | ### Bug fixes
46 |
47 | Bug reports are appreciated ahead of bug fixes as early reporting allows the community to be aware
48 | of any issues ahead of a fix being submitted. If you intend to fix a bug after reporting, that is
49 | greatly appreciated - just make sure to mention you intend to work on it on the issue report so the
50 | maintainers are aware and leave you the chance to make a contribution.
51 |
52 | When submitting a bug fix PR, a test must be added (or an existing test modified) that exposes the
53 | bug and your change must make that test pass.
54 |
55 | ### New features
56 |
57 | Issues should be opened ahead of submitting a PR to added a new feature. This is to prevent you
58 | wasting your time should a feature not be desirable and allows others to have input into the
59 | conversation.
60 |
61 | All new functionality must be fully tested and new public functions must include an
62 | [`Example` test](https://go.dev/blog/examples) that will be used by the reference docs to
63 | demonstrate its use.
64 |
65 | Mark pull requests as "Draft" if you intend to use the pull request as a workspace but are not yet
66 | ready to receive unsolicited feedback on specifics like commit messages or failing tests.
67 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Tom Godkin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .phony: test check lint
2 |
3 | GO_BINARY ?= go
4 |
5 | check: lint
6 | @$(GO_BINARY) vet ./...
7 | @gofmt -l .
8 | @test -z "$$( gofmt -l . )"
9 |
10 | lint:
11 | ifndef SKIP_LINT
12 | @golangci-lint run ./...
13 | endif
14 |
15 | test:
16 | $(GO_BINARY) test -race -v ./...
17 |
18 | cov: SHELL:=/bin/bash
19 | cov:
20 | $(GO_BINARY) test -race -coverprofile=coverage.txt -covermode=atomic $$( $(GO_BINARY) list ./... | grep -v assert | grep -v fakes )
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Functional Programming in Go
2 |
3 | [](https://github.com/BooleanCat/go-functional/releases)
4 | [](https://github.com/BooleanCat/go-functional/actions)
5 | [](https://pkg.go.dev/github.com/BooleanCat/go-functional/v2)
6 | [](https://goreportcard.com/report/github.com/BooleanCat/go-functional/v2)
7 | [](https://codecov.io/gh/BooleanCat/go-functional)
8 |
9 | A library of iterators for use with [iter.Seq](https://pkg.go.dev/iter#Seq). Requires Go 1.23+.
10 |
11 | ```go
12 | // The first 5 natural numbers
13 | numbers := slices.Collect(
14 | it.Take(it.NaturalNumbers[int](), 5),
15 | )
16 |
17 | // All even numbers
18 | evens := it.Filter(it.NaturalNumbers[int](), filter.IsEven)
19 |
20 | // String representations of integers
21 | numbers := it.Map(it.NaturalNumbers[int](), strconv.Itoa)
22 | ```
23 |
24 | _[Reference documentation](https://pkg.go.dev/github.com/BooleanCat/go-functional/v2)_
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ## Installation
33 |
34 | ```terminal
35 | go get github.com/BooleanCat/go-functional/v2@latest
36 | ```
37 |
38 | ## Overview
39 |
40 | Most functions offered by this package are either consumers or iterators.
41 |
42 | [Consumers](#consumers) will iterate over an iterator and completely or partially drain them of
43 | values and (in most cases) collect the values into a data type.
44 |
45 | [Iterators](#iterators) are functions that yield new values and can be ranged over. See Go's
46 | documentation for iterators for more details.
47 |
48 | ## Consumers
49 |
50 | The standard libary provides functions to collect iterators in the
51 | [slices](https://pkg.go.dev/slices) and [maps](https://pkg.go.dev/maps) packages that should satisfy
52 | most cases where collection is needed.
53 |
54 | This package provides additional collection methods and makes existing consumers from the standard
55 | library chainable.
56 |
57 |
58 | > [!WARNING]
59 | > Attempting to collect infinite iterators will cause an infinite loop and likely deadlock. Consider
60 | > bounding infinite iterators before collect (for example using [Take](#take)).
61 |
62 | ### All & Any
63 |
64 | `All` will return `true` if all values yielded by an iterator are `true`, or `false` otherwise.
65 | Iteration will terminate early if `false` is encountered. Empty iterators will return `true`.
66 |
67 | ```go
68 | it.All(slices.Values([]bool{true, false, true})) // false
69 | ```
70 |
71 | `Any` will return `true` if any value yielded by an iterator is `true`, or `false` otherwise.
72 | Iteration will terminate early if `true` is encountered. Empty iterators will return `false`.
73 |
74 | ```go
75 | it.Any(slices.Values([]bool{false, false, true})) // true
76 | ```
77 |
78 |
79 | > [!NOTE]
80 | > The `itx` package does not contain `All` or `Any` due to limitations with Go's type system.
81 |
82 | ### Collect
83 |
84 | In most cases [slices.Collect](https://pkg.go.dev/slices#Collect) from the standard library may be
85 | used to collect items from an iterator into a slice. There are several other variants of collect
86 | available for use for different use cases.
87 |
88 | ```go
89 | // Chainable
90 | numbers := itx.NaturalNumbers[int]().Take(5).Collect()
91 |
92 | // Collect an iter.Seq2[V, W] into two slices
93 | keys, values := it.Collect2(maps.All(map[string]int{"one": 1, "two": 2}))
94 |
95 | // As above, but chainable
96 | keys, values := itx.FromMap(map[string]int{"one": 1, "two": 2}).Collect()
97 | ```
98 |
99 |
TryCollect & MustCollect
100 |
101 | Dealing with iterators that return `T, error` can involve the boilerplate of checking that the
102 | returned slice of errors only contains `nil`. `TryCollect` solves this by collecting all values into
103 | a slice and returning a single error: the first one encountered.
104 |
105 | ```go
106 | text := strings.NewReader("one\ntwo\nthree\n")
107 |
108 | if lines, err := it.TryCollect(it.LinesString(text)); err != nil {
109 | fmt.Println(lines)
110 | }
111 | ```
112 |
113 | `MustCollect` is similar except that if an error is encountered then a panic will occur.
114 |
115 | ```go
116 | text := strings.NewReader("one\ntwo\nthree\n")
117 |
118 | lines := it.MustCollect(it.LinesString(text))
119 | ```
120 |
121 |
122 | > [!TIP]
123 | > Use `MustCollect` when you can guarantee that no error will occur (such as with
124 | > [strings.Reader](https://pkg.go.dev/strings#Reader)).
125 |
126 |
127 | > [!NOTE]
128 | > If an error is encountered, collection stops. This means the iterator being collected may not be
129 | > fully drained.
130 |
131 |
132 | > [!NOTE]
133 | > The `itx` package does not contain `TryCollect` or `MustCollect` due to limitations with Go's type
134 | > system.
135 |
136 | ### ForEach
137 |
138 | ForEach consumes an iterator and applies a function to each value yielded.
139 |
140 | ```go
141 | it.ForEach(slices.Values([]int{1, 2, 3}), func(number int) {
142 | fmt.Println(number)
143 | })
144 |
145 | // Chainable
146 | itx.FromSlice([]int{1, 2, 3}).ForEach(func(number int) {
147 | fmt.Println(number)
148 | })
149 |
150 | // For each member of an iter.Seq2
151 | it.ForEach2(slices.All([]int{1, 2, 3}), func(index int, number int) {
152 | fmt.Println(index, number)
153 | })
154 |
155 | // As above, but chainable
156 | itx.FromSlice([]int{1, 2, 3}).Enumerate().ForEach(func(index int, number int) {
157 | fmt.Println(index, number)
158 | })
159 | ```
160 |
161 | ### Fold
162 |
163 | Fold every element into an accumulator by applying a function and passing an initial value.
164 |
165 | ```go
166 | it.Fold(slices.Values([]int{1, 2, 3}), op.Add, 0)
167 |
168 | // Fold an iter.Seq2
169 | it.Fold2(slices.All([]int{1, 2, 3}), func(i, a, b int) int {
170 | return i + 1
171 | }, 0)
172 | ```
173 |
174 |
175 | > [!TIP]
176 | > The [op package](it/op/op.go) contains some simple, pre-defined operation functions.
177 |
178 |
179 | > [!NOTE]
180 | > The `itx` package does not contain `Fold` due to limitations with Go's type system.
181 |
182 | ### Max & Min
183 |
184 | `Max` and `Min` consume an iterator and return the maximum or minimum value yielded and true if the
185 | iterator contained at least one value, or the zero value and false if the iterator was empty.
186 |
187 | The type of the value yielded by the iterator must be `comparable`.
188 |
189 | ```go
190 | max, ok := it.Max(slices.Values([]int{1, 2, 3}))
191 | min, ok := it.Min(slices.Values([]int{1, 2, 3}))
192 | ```
193 |
194 |
195 | > [!NOTE]
196 | > The `itx` package does not contain `Max` or `Min` due to limitations with Go's type system.
197 |
198 | ### Len
199 |
200 | Len consumes an iterator and returns the number of values yielded.
201 |
202 | ```go
203 | it.Len(slices.Values([]int{1, 2, 3}))
204 |
205 | // Chainable
206 | itx.FromSlice([]int{1, 2, 3}).Len()
207 |
208 | // Len of an iter.Seq2
209 | it.Len2(slices.All([]int{1, 2, 3}))
210 |
211 | // As above, but chainable
212 | itx.FromSlice([]int{1, 2, 3}).Enumerate().Len()
213 | ```
214 |
215 | ### Find
216 |
217 | Find consumes an iterator until a value is found that satisfies a predicate. It returns the value
218 | and true if one was found, or the zero value and false if the iterator was exhausted before a value
219 | was found.
220 |
221 | ```go
222 | found, ok := it.Find(slices.Values([]int{1, 2, 3}), func(i int) bool {
223 | return i == 2
224 | })
225 |
226 | // Chainable
227 | value, ok := itx.FromSlice([]int{1, 2, 3}).Find(func(number int) bool {
228 | return number == 2
229 | })
230 |
231 | // Finding within an iter.Seq2
232 | index, value, ok := it.Find2(slices.All([]int{1, 2, 3}), func(i, v int) bool {
233 | return i == 2
234 | })
235 |
236 | // As above, but chainable
237 | index, value, ok := itx.FromSlice([]int{1, 2, 3}).Enumerate().Find(func(index int, number int) bool {
238 | return index == 1
239 | })
240 | ```
241 |
242 |
243 | > [!TIP]
244 | > The [filter package](it/filter/filter.go) contains some simple, pre-defined predicate functions.
245 |
246 | ### Contains
247 |
248 | Contains consumes an iterator until the provided value is found, returning true if the value was
249 | found or false if the iterator was exhausted.
250 |
251 | ```go
252 | ok := it.Contains(slices.Values([]int{1, 2, 3}), 2)
253 | ```
254 |
255 |
256 | > [!NOTE]
257 | > The `itx` package does not contain `Contains` due to limitations with Go's type system.
258 |
259 | ### From, FromSlice, FromMap & Seq
260 |
261 | The itx package contains some helper functions to convert iterators, slices or maps directly into
262 | chainable iterators to avoid some boilerplate.
263 |
264 | ```go
265 | itx.From(slices.Values([]int{1, 2, 3})).Collect()
266 |
267 | itx.From2(maps.All(map[int]string{1: "one"}))
268 |
269 | itx.FromSlice([]int{1, 2, 3}).Collect()
270 |
271 | itx.FromMap(map[int]int{1: 2}).Collect()
272 | ```
273 |
274 | The `itx` package also contains a helper function Seq that will convert a chainable iterator into an
275 | [iter.Seq](https://pkg.go.dev/iter#Seq) so that it can be used in functions that accept that type
276 | (such as the standard library).
277 |
278 | The standard library functions that work with iterators (such as
279 | [slices.Collect](https://pkg.go.dev/slices#Collect)) accept the
280 | [iter.Seq](https://pkg.go.dev/iter#Seq) family of types. This precludes those functions from
281 | accepting types with another alias (such as `itx.Iterator`) with the same type definition. This
282 | means it is necessary to "covert" an `itx.Iterator` into an [iter.Seq](https://pkg.go.dev/iter#Seq)
283 | before passing the iterator into those functions.
284 |
285 | ```go
286 | slices.Collect(itx.NaturalNumbers[int]().Take(3).Seq())
287 | ```
288 |
289 |
290 | > [!TIP]
291 | > go-functional's functions that accept iterators always accept `func(func(V) bool)` or
292 | > `func(func(V, W) bool)` rather than any specific type alias so that they can accept any type alias
293 | > with the definitions [iter.Seq](https://pkg.go.dev/iter#Seq),
294 | > [iter.Seq2](https://pkg.go.dev/iter#Seq2), `itx.Iterator`, `itx.Iterator2` or any other
295 | > third-party types aliased to the same type.
296 |
297 | ### ToChannel
298 |
299 | ToChannel sends yielded values to a channel.
300 |
301 | The channel is closed when the iterator is exhausted. Beware of leaked go routines when using this
302 | function with an infinite iterator.
303 |
304 | ```go
305 | channel := it.ToChannel(slices.Values([]int{1, 2, 3}))
306 |
307 | for number := range channel {
308 | fmt.Println(number)
309 | }
310 |
311 | // Chainable
312 | channel := itx.FromSlice([]int{1, 2, 3}).ToChannel()
313 |
314 | for number := range channel {
315 | fmt.Println(number)
316 | }
317 | ```
318 |
319 |
320 | > [!NOTE]
321 | > Unlike most consumers, the iterator is not immediately consumed by ToChannel. Instead is it
322 | > consumed as values are pulled from the channel.
323 |
324 | ### Drain
325 |
326 | Drain consumes an iterator's values and drops them.
327 |
328 | ```go
329 | printValue := func(n int) int {
330 | fmt.Println(n)
331 | return n
332 | }
333 |
334 | it.Drain(it.Map(slices.Values([]int{1, 2, 3}), printValue))
335 |
336 | // Chainable
337 | itx.From(it.Map(slices.Values([]int{1, 2, 3}), printValue)).Drain()
338 |
339 | // Drain an iter.Seq2
340 | printValue2 := func(i, n int) (int, int) {
341 | fmt.Println(n)
342 | return i, n
343 | }
344 |
345 | it.Drain2(it.Map2(slices.All([]int{1, 2, 3}), printValue2))
346 |
347 | // As above, but chainable
348 | itx.From2(it.Map2(slices.All([]int{1, 2, 3}), printValue2)).Drain()
349 | ```
350 |
351 |
352 | > [!TIP]
353 | > Use `Drain` to consume an iterator to invoke any side effects when you don't need to collect the
354 | > values.
355 |
356 | ## Iterators
357 |
358 | This library contains two kinds of iterators in the `it` and `itx` packages. In most cases you'll
359 | find the same iterators in each package, the difference between them being that the iterators in the
360 | `itx` package can be "dot-chained" (e.g. `iter.Filter(...).Take(3).Collect()`) and those in `it`
361 | cannot.
362 |
363 | Iterators within the `it` package are of the type [iter.Seq](https://pkg.go.dev/iter#Seq) or
364 | [iter.Seq2](https://pkg.go.dev/iter#Seq2) (from the standard library). Iterators within the `itx`
365 | package are of the type `itx.Iterator[V]` or `itx.Iterator2[V, W]`.
366 |
367 | There are two important factors to consider when using iterators:
368 |
369 | 1. Some iterators yield an infinite number of values and care should be taken to avoid consuming
370 | (using functions such as [slices.Collect](https://pkg.go.dev/slices#Collect)) otherwise it's
371 | likely to cause an infinite while loop.
372 | 2. Many iterators take another iterator as an argument (such as [Filter](#filter) or Map). Avoid
373 | using an iterator after it has been passed to another iterator otherwise you'll risk multiple
374 | functions consuming a single (likely not thread-safe) iterator and causing confusing and
375 | difficult to debug behaviour.
376 |
377 | ### Chain
378 |
379 | Chain yields values from multiple iterators in the sequence they are provided in. Think of it as
380 | glueing many iterators together.
381 |
382 | When provided zero iterators it will behave like [Exhausted](#exhausted).
383 |
384 | ```go
385 | numbers := it.Chain(slices.Values([]int{1, 2}), slices.Values([]int{3, 4}))
386 |
387 | pairs := itx.FromSlice([]int{1, 2}).Chain(slices.Values([]int{3, 4}))
388 |
389 | pairs := it.Chain2(maps.All(map[string]int{"a": 1}), maps.All(map[string]int{"b": 2}))
390 |
391 | pairs := itx.FromMap(map[string]int{"a": 1}).Chain(maps.All(map[string]int{"b": 2}))
392 | ```
393 |
394 | ### FromChannel
395 |
396 | FromChannel pulls values from a channel and yields them via an iterator. The usual concerns around
397 | channel deadlocks apply here.
398 |
399 | The iterator is exhausted when the channel is closed and it is the responsibility of the caller to
400 | close the channel.
401 |
402 | ```go
403 | items := make(chan int)
404 |
405 | go func() {
406 | defer close(items)
407 | items <- 1
408 | items <- 2
409 | }()
410 |
411 | for number := range it.FromChannel(items) {
412 | fmt.Println(number)
413 | }
414 |
415 | // Chainable
416 | items := make(chan int)
417 |
418 | go func() {
419 | defer close(items)
420 | items <- 1
421 | items <- 0
422 | }()
423 |
424 | for number := range itx.FromChannel(items).Exclude(filter.IsZero) {
425 | fmt.Println(number)
426 | }
427 | ```
428 |
429 |
430 | > [!WARNING]
431 | > In order to prevent a deadlock, the channel must be closed before attemping to stop the iterator
432 | > when it's used in a pull style. See [iter.Pull](https://pkg.go.dev/iter#Pull).
433 |
434 | ### Compact
435 |
436 | Compact yields all values from a delegate iterator that are not zero values. It is functionally
437 | equivalent to `it.Exclude(delegate, filter.IsZero)`.
438 |
439 | ```go
440 | words := it.Compact(slices.Values([]string{"foo", "", "bar", "", ""}))
441 | ```
442 |
443 |
444 | > [!NOTE]
445 | > The `itx` package does not contain `Compact` due to limitations with Go's type system.
446 |
447 | ### Cycle
448 |
449 | Cycle yields all values from an iterator before returning to the beginning and yielding all values
450 | again (indefinitely).
451 |
452 | ```go
453 | numbers := it.Take(it.Cycle(slices.Values([]int{1, 2})), 5)
454 |
455 | // Chainable
456 | numbers := itx.FromSlice([]int{1, 2}).Cycle().Take(5)
457 |
458 | // Cycling an iter.Seq2
459 | numbers := it.Take2(it.Cycle2(maps.All(map[int]string{1: "one"})), 5)
460 |
461 | // As above, but chainable
462 | numbers := itx.FromMap(map[int]string{1: "one"}).Cycle().Take(5)
463 | ```
464 |
465 |
466 | > [!NOTE]
467 | > Since cycle needs to store all values yielded in memory, its memory usage will grow as the first
468 | > cycle is consumed and remain at a constant size on subsequent cycles.
469 |
470 |
471 | > [!WARNING]
472 | > This iterator yields an infinite number of values and care should be taken when consuming it
473 | > otherwise it's likely to result in an infinite while loop. Consider bounding the size of the
474 | > iterator before consuming (e.g. using [Take](#take)).
475 |
476 |
Drop & DropWhile
477 |
478 | Drop yields all values from a delegate iterator after dropping a number of values from the
479 | beginning. Values are not dropped immediately, but when consumption begins.
480 |
481 | When dropping a number of values larger than the length of the iterator, it behaves like
482 | [Exhausted](#exhausted).
483 |
484 | ```go
485 | numbers := it.Drop(slices.Values([]int{1, 2, 3, 4, 5}), 2)
486 |
487 | // Chainable
488 | numbers := itx.FromSlice([]int{1, 2, 3, 4, 5}).Drop(2)
489 |
490 | // Dropping on iter.Seq2
491 | numbers := it.Drop2(maps.All(map[int]string{1: "one", 2: "two", 3: "three"}), 1)
492 |
493 | // As above, but chainable
494 | numbers := itx.FromMap(map[int]string{1: "one", 2: "two", 3: "three"}).Drop(1)
495 | ```
496 |
497 | DropWhile drops values from the provided iterator whilst the predicate returns true for each value.
498 | After the first value results in the predicate returning false, the iterator resumes as normal.
499 |
500 | ```go
501 | slices.Collect(it.DropWhile(slices.Values([]int{1, 2, 3, 4, 5}), filter.LessThan(3)))
502 |
503 | // Chainable
504 | itx.FromSlice([]int{1, 2, 3, 4, 5}).DropWhile(filter.LessThan(3)).Collect()
505 |
506 | lessThanThree := func(string, number int) { return number < 3 }
507 |
508 | // Taking from iter.Seq2
509 | maps.Collect(it.DropWhile2(maps.All(map[string]int{"one": 1, "four": 4}), lessThanThree))
510 |
511 | // As above, but chainable
512 | itx.FromMap(map[string]int{"one": 1, "four": 4}).DropWhile(lessThanThree).Collect()
513 | ```
514 |
515 | ### Enumerate
516 |
517 | Enumerating an [iter.Seq](https://pkg.go.dev/iter#Seq) like iterator returns an
518 | [iter.Seq2](https://pkg.go.dev/iter#Seq2) like iterator yielding the index of each value and the
519 | value.
520 |
521 | ```go
522 | indexedValues := it.Enumerate(slices.Values([]int{1, 2, 3}))
523 |
524 | // Chainable
525 | indexedValues := itx.FromSlice([]int{1, 2, 3}).Enumerate()
526 | ```
527 |
528 |
529 | > [!TIP]
530 | > When iterating over a slice and immediately enumerating, consider instead using the standard
531 | > library's [slices.All](https://pkg.go.dev/slices#All) function rather than this.
532 |
533 | ### Exhausted
534 |
535 | Exhausted is an iterator that yields no values.
536 |
537 | ```go
538 | slices.Collect(it.Exhausted[int]())
539 |
540 | // Chainable
541 | it.Exhausted[int]().Collect()
542 |
543 | // Exhausted iter.Seq2
544 | maps.Collect(it.Exhausted2[int, string]())
545 |
546 | // As above, but chainable
547 | itx.Exhausted2[int, string]().Collect()
548 | ```
549 |
550 |
Filter & Exclude
551 |
552 | Filter yields values from an iterator that satisfy a predicate. Likewise, exclude yields values from
553 | an iterator that do not satisfy a predicate.
554 |
555 | ```go
556 | it.Filter(slices.Values([]int{1, 2, 3, 4, 5}), filter.IsEven)
557 | it.Exclude(slices.Values([]int{1, 2, 3, 4, 5}), filter.IsEven)
558 |
559 | // Chainable
560 | itx.FromSlice([]int{1, 2, 3, 4, 5}).Filter(filter.IsEven)
561 | itx.FromSlice([]int{1, 2, 3, 4, 5}).Exclude(filter.IsEven)
562 |
563 | // Filtering an iter.Seq2
564 | isOne := func(n int, _ string) bool { return n == 1 }
565 |
566 | it.Filter2(maps.All(map[int]string{1: "one", 2: "two", 3: "three"}), isOne)
567 | it.Exclude2(maps.All(map[int]string{1: "one", 2: "two", 3: "three"}), isOne)
568 |
569 | // As above, but chainable
570 | itx.FromMap(map[int]string{1: "one", 2: "two", 3: "three"}).Filter(isOne)
571 | itx.FromMap(map[int]string{1: "one", 2: "two", 3: "three"}).Exclude(isOne)
572 | ```
573 |
574 |
575 | > [!TIP]
576 | > The [filter package](it/filter/filter.go) contains some simple, pre-defined predicate functions.
577 |
578 | ### FilterError & ExcludeError
579 |
580 | Similar to [Filter and Exclude](#filter), these functions filter values from an iterator using a
581 | predicate that may return an error.
582 |
583 | ```go
584 | isFoo := func(s string) (bool, error) { return s == "foo", nil }
585 |
586 | values, err := it.TryCollect(it.FilterError(slices.Values([]string{"foo", "bar"}), isFoo))
587 | values, err := it.TryCollect(it.ExcludeError(slices.Values([]string{"foo", "bar"}), isFoo))
588 |
589 | // Chainable
590 | values, err := it.TryCollect(itx.FromSlice([]int{"foo", "bar"}).FilterError(isFoo))
591 | values, err := it.TryCollect(itx.FromSlice([]int{"foo", "bar"}).ExcludeError(isFoo))
592 | ```
593 |
594 | ### FilterUnique
595 |
596 | FilterUnique yields only the unique values from an iterator.
597 |
598 | ```go
599 | values := it.FilterUnique(slices.Values([]int{1, 2, 2, 3, 3, 3, 4}))
600 | ```
601 |
602 |
603 | > [!WARNING]
604 | > Unique values are stored in memory until the iterator is exhausted. Large iterators with
605 | > many unique values may use a large amount of memory.
606 |
607 |
608 | > [!NOTE]
609 | > The `itx` package does not contain `FilterUnique` due to limitations with Go's type system.
610 |
611 | ### Integers
612 |
613 | Integers yields all integers in the range [start, stop) with the given step.
614 |
615 | ```go
616 | for i := range it.Integers[uint](0, 3, 1) {
617 | fmt.Println(i)
618 | }
619 |
620 | // Chainable
621 | for i := range itx.Integers[uint](0, 3, 1).Drop(1) {
622 | fmt.Println(i)
623 | }
624 | ```
625 |
626 | ### Lines & LinesString
627 |
628 | Lines yields lines from an [io.Reader](https://pkg.go.dev/io#Reader). Lines are split using the
629 | standard library's [bufio.Scanner](https://pkg.go.dev/bufio#Scanner). Each value yielded from the
630 | iterator is a line from the provided reader. Empty lines will result in empty values.
631 |
632 | Since reading from an [io.Reader](https://pkg.go.dev/io#Reader) can fail, each line is returned with
633 | a corresponding `error` value.
634 |
635 | LinesString behaves exactly like Lines except that its values are strings rather than byte slices.
636 |
637 | ```go
638 | buffer := strings.NewReader("one\ntwo\nthree\n")
639 |
640 | for line, err := range it.Lines(buffer) {
641 | if err != nil {
642 | fmt.Println(err)
643 | break
644 | }
645 |
646 | fmt.Println(string(line))
647 | }
648 |
649 | for line, err := range it.LinesString(buffer) {
650 | if err != nil {
651 | fmt.Println(err)
652 | break
653 | }
654 |
655 | fmt.Println(line)
656 | }
657 | ```
658 |
659 |
660 | > [!TIP]
661 | > Consider using [TryCollect](#trycollect) in conjunction with Lines to make error collection less
662 | > cumbersome:
663 | >
664 | > ```go
665 | > lines, err := it.TryCollect(it.LinesString(buffer))
666 | > ```
667 |
668 |
669 | > [!TIP]
670 | > For cases where errors will never result from reading, such as with
671 | > [bytes.Buffer](https://pkg.go.dev/bytes#Buffer), consider dropping the error value before
672 | > collection:
673 | >
674 | > ```go
675 | > lines := itx.LinesString(buffer).Left().Collect()
676 | > ```
677 |
678 |
679 | > [!WARNING]
680 | > As with [bufio.Scanner](https://pkg.go.dev/bufio#Scanner), there is a maximum line length per line
681 | > of 65536 characters.
682 |
683 |
Map & Transform
707 | > [!NOTE]
708 | > The `itx` package does not contain `Map` due to limitations with Go's type system. Instead a
709 | > limited from of `Map` called `Transform` is provided where the type returned from operations is
710 | > the same as a type of the iterator's values.
711 | >
712 | > A chainable Map will be added should Go's type system ever support new generic type parameters on
713 | > methods.
714 |
715 |
716 | > [!TIP]
717 | > If you wish to chain operations on `Map`, you can do so by first converting it to an
718 | > `itx.Iterator` like so:
719 | >
720 | > ```go
721 | > itx.From(it.Map(slices.Values([]int{1, 2, 3}), double)).Collect()
722 | > ```
723 |
724 | ### MapError & TransformError
725 |
726 | `MapError` and `TransformError` behave like [Map and Transform](#map) except that they accept a
727 | function that may return an error.
728 |
729 | ```go
730 | double := func(n int, err error) int { return n * 2, nil }
731 |
732 | it.MapError(slices.Values([]int{1, 2, 3}), double)
733 |
734 | // Limited chainable flavour of MapError
735 | itx.FromSlice([]int{1, 2, 3}).TransformError(double)
736 | ```
737 |
738 |
739 | > [!NOTE]
740 | > The `itx` package does not contain `MapError` due to limitations with Go's type system. Instead a
741 | > limited from of `MapError` called `TransformError` is provided where the type returned from
742 | > operations is the same as a type of the iterator's values.
743 | >
744 | > A chainable MapError will be added should Go's type system ever support new generic type
745 | > parameters on methods.
746 |
747 |
748 | > [!TIP]
749 | > If you wish to chain operations on `MapError`, you can do so by first converting it to an
750 | > `itx.Iterator2` like so:
751 | >
752 | > ```go
753 | > itx.From2(it.MapError(slices.Values([]int{1, 2, 3}), double)).Collect()
754 | > ```
755 |
756 | ### NaturalNumbers
757 |
758 | NaturalNumbers yields all non-negative integers in ascending order.
759 |
760 | ```go
761 | for i := range it.Take(it.NaturalNumbers[int](), 3) {
762 | fmt.Println(i)
763 | }
764 |
765 | // Chainable
766 | for i := range itx.NaturalNumbers[int]().Take(3) {
767 | fmt.Println(i)
768 | }
769 | ```
770 |
771 |
772 | > [!WARNING]
773 | > This iterator yields an infinite number of values and care should be taken when consuming it
774 | > otherwise it's likely to result in an infinite while loop. Consider bounding the size of the
775 | > iterator before consuming (e.g. using [Take](#take)).
776 |
777 |
778 | > [!WARNING]
779 | > There is no protection against overflowing whatever integer type is used for this iterator.
780 |
781 | ### Once
782 |
783 | Once yields the provided value once before being exhausted.
784 |
785 | ```go
786 | slices.Collect(it.Once(42))
787 |
788 | // Chainable
789 | itx.Once(42).Collect()
790 |
791 | // For iter.Seq2
792 | maps.Collect(it.Once2(42, "42"))
793 |
794 | // As above, but chainable
795 | itx.Once(42, "42").Collect()
796 | ```
797 |
798 | ### Repeat
799 |
800 | Repeat yields the same value indefinitely.
801 |
802 | ```go
803 | slices.Collect(it.Take(it.Repeat(42), 5))
804 |
805 | // Chainable
806 | itx.Repeat(42).Take(5).Collect()
807 |
808 | // For iter.Seq2
809 | maps.Collect(it.Take2(it.Repeat(42, "42"), 5))
810 |
811 | // As above, but chainable
812 | itx.Repeat2(42, "42").Take(5).Collect()
813 | ```
814 |
815 |
816 | > [!WARNING]
817 | > This iterator yields an infinite number of values and care should be taken when consuming it
818 | > otherwise it's likely to result in an infinite while loop. Consider bounding the size of the
819 | > iterator before consuming (e.g. using [Take](#take)).
820 |
821 |