go-functional

repository·main·Indexed 19 days ago

https://github.com/booleancat/go-functional

A library of iterators and consumers for Go 1.23+ designed to work with iter.Seq. It provides functional programming primitives such as Map, Filter, Fold, and Collect. The library includes an 'it' package for standard functional operations and an 'itx' package for dot-chaining support via custom Iterator types.

Tokens
5.1K
Snippets
21
Records
22
Agent score
19%

What's inside go-functional

  1. Understand the difference between `it` and `itx` iterators

    main

    The library provides two distinct packages for iterators:

    1. it package: Uses standard library types iter.Seq or iter.Seq2. These are functional but do not support method chaining.
    2. itx package: Uses custom types itx.Iterator[V] or itx.Iterator2[V, W]. These allow for dot-chaining (e.g., iter.Filter(...).Take(3).Collect()).

    Critical Usage Warnings

    • Infinite Iterators: Some iterators (like Cycle, Repeat, or NaturalNumbers) yield infinite values. Avoid using functions like slices.Collect directly on them without bounding the size (e.g., using Take), otherwise, you will trigger an infinite loop.
    • Iterator Consumption: Many iterators take another iterator as an argument. Do not reuse an iterator after passing it to another function; doing so risks multiple functions attempting to consume a single, non-thread-safe iterator, leading to difficult-to-debug behavior.
    // it package (non-chainable)
    numbers := it.Chain(slices.Values([]int{1, 2}), slices.Values([]int{3, 4}))
    
    // itx package (chainable)
    numbers := itx.FromSlice([]int{1, 2}).Chain(slices.Values([]int{3, 4}))
  2. How consumers and iterators work together

    main

    The library is built around two main types of functions:

    1. Iterators: Functions that yield new values and can be ranged over (compatible with Go's iter.Seq).
    2. Consumers: Functions that iterate over an iterator, often draining it completely or partially to collect values into a data type.

    Warning: Attempting to collect infinite iterators will cause an infinite loop and likely deadlock. Always bound infinite iterators (e.g., using Take) before calling a consumer like Collect.

  3. Combine multiple iterators with `Chain`

    main

    The Chain function glues multiple iterators together, yielding values from each in the sequence they are provided. If zero iterators are provided, it behaves like Exhausted.

    // Using it package
    numbers := it.Chain(slices.Values([]int{1, 2}), slices.Values([]int{3, 4}))
    
    // Using itx package (chainable)
    pairs := itx.FromSlice([]int{1, 2}).Chain(slices.Values([]int{3, 4}))
    
    // Using maps
    pairs := it.Chain2(maps.All(map[string]int{"a": 1}), maps.All(map[string]int{"b": 2}))
  4. Use All and Any to check predicate conditions

    main

    Use it.All and it.Any to evaluate boolean conditions across an iterator.

    • it.All: Returns true if all values are true. Returns true for empty iterators. Terminates early if false is encountered.
    • it.Any: Returns true if any value is true. Returns false for empty iterators. Terminates early if true is encountered.

    Note: These are not available in the itx package due to Go type system limitations.

    it.All(slices.Values([]bool{true, false, true}))  // false
    it.Any(slices.Values([]bool{false, false, true}))  // true
  5. Limit iterator size with `Take` and `TakeWhile`

    main

    Take

    Limits the iterator to a specified number of values. If the iterator is shorter than the limit, it simply exhausts normally.

    TakeWhile

    Yields values as long as the predicate returns true. Once the predicate returns false, the iterator is exhausted.

    // Take
    // Returns [1, 2]
    slices.Collect(it.Take(slices.Values([]int{1, 2, 3}), 2))
    
    // TakeWhile
    // Returns [1, 2]
    slices.Collect(it.TakeWhile(slices.Values([]int{1, 2, 3, 4}), filter.LessThan(3)))
  6. Convert iterators to chainable types or channels

    main

    To bridge the gap between standard library iterators and the library's chainable API, or to use iterators in concurrent code:

    • itx.From, itx.FromSlice, itx.FromMap: Converts standard types/iterators into itx.Iterator for chaining.
    • itx.Seq(): Converts a chainable itx.Iterator back into a standard iter.Seq so it can be used with slices.Collect or for...range.
    • it.ToChannel: Sends yielded values to a channel. The channel is closed when the iterator is exhausted. Note: The iterator is not immediately consumed; it is consumed as values are pulled from the channel. Be careful with infinite iterators to avoid leaking goroutines.
    // Convert to chainable
    numbers := itx.FromSlice([]int{1, 2, 3}).Take(2).Seq()
    
    // Convert to standard Seq for slices.Collect
    res := slices.Collect(itx.NaturalNumbers[int]().Take(3).Seq())
    
    // Convert to channel
    channel := it.ToChannel(slices.Values([]int{1, 2, 3}))
    for number := range channel {
    	fmt.Println(number)
    }
  7. Use Drain to trigger side effects without collecting values

    main

    If you need to iterate over an iterator solely to trigger side effects (like printing or logging) but do not need the resulting values, use Drain or Drain2. This effectively consumes the iterator and discards the output.

    // Drain a single sequence
    it.Drain(it.Map(slices.Values([]int{1, 2, 3}), func(n int) int { 
        fmt.Println(n); return n 
    }))
    
    // Chainable Drain
    itx.From(it.Map(slices.Values([]int{1, 2, 3}), printValue)).Drain()
  8. Create infinite repeating sequences with `Cycle` and `Repeat`

    main

    Cycle

    Yields all values from an iterator before returning to the beginning and yielding all values again indefinitely. It stores all values in memory, so memory usage grows until the first cycle is complete.

    Repeat

    Yields the same value indefinitely.

    WARNING: Both are infinite. Always use Take to bound the consumption to avoid infinite loops.

    // Bounding a Cycle
    numbers := it.Take(it.Cycle(slices.Values([]int{1, 2})), 5)
    
    // Chainable Cycle
    numbers := itx.FromSlice([]int{1, 2}).Cycle().Take(5)
    
    // Bounding a Repeat
    slices.Collect(it.Take(it.Repeat(42), 5))
    
    // Chainable Repeat
    itx.Repeat(42).Take(5).Collect()
  9. Add indices to iterators with `Enumerate`

    main

    The Enumerate function transforms an iter.Seq into an iter.Seq2, yielding the index of each value alongside the value itself.

    // it package
    indexedValues := it.Enumerate(slices.Values([]int{1, 2, 3}))
    
    // itx package (chainable)
    indexedValues := itx.FromSlice([]int{1, 2, 3}).Enumerate()
  10. Skip values with `Drop` and `DropWhile`

    main

    Drop

    Yields values from a delegate iterator after skipping a specified number of values from the beginning. If the drop count is larger than the iterator length, it behaves like Exhausted.

    DropWhile

    Drops values from the iterator as long as the provided predicate returns true. Once the predicate returns false, the iterator resumes normal yielding.

    // Drop
    numbers := it.Drop(slices.Values([]int{1, 2, 3, 4, 5}), 2)
    // Chainable
    numbers := itx.FromSlice([]int{1, 2, 3, 4, 5}).Drop(2)
    
    // DropWhile
    // Returns [3, 4, 5]
    slices.Collect(it.DropWhile(slices.Values([]int{1, 2, 3, 4, 5}), filter.LessThan(3)))
    
    // Chainable DropWhile
    itx.FromSlice([]int{1, 2, 3, 4, 5}).DropWhile(filter.LessThan(3)).Collect()
  11. Create an iterator from a channel with `FromChannel`

    main

    The FromChannel function pulls values from a channel and yields them via an iterator.

    Important:

    • The iterator is exhausted when the channel is closed.
    • It is the caller's responsibility to close the channel.
    • To prevent deadlocks when using pull-style consumption (like iter.Pull), the channel must be closed before attempting to stop the iterator.
    items := make(chan int)
    
    go func() {
    	defer close(items)
    	items <- 1
    	items <- 2
    }()
    
    for number := range it.FromChannel(items) {
    	fmt.Println(number)
    }
    
    // Chainable version
    for number := range itx.FromChannel(items).Exclude(filter.IsZero) {
    	fmt.Println(number)
    }