Initial commit

This commit is contained in:
Lily Rose 2025-08-09 18:40:17 +10:00
commit 8c5aa8092d
7 changed files with 1462 additions and 0 deletions

23
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,23 @@
name: test
on:
push:
branches:
- master
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: "27.1.2"
gleam-version: "1.11.1"
rebar3-version: "3"
# elixir-version: "1"
- run: gleam deps download
- run: gleam test
- run: gleam format --check src test

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump

26
README.md Normal file
View file

@ -0,0 +1,26 @@
# infiniyield
[![Package Version](https://img.shields.io/hexpm/v/infiniyield)](https://hex.pm/packages/infiniyield)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/infiniyield/)
```sh
gleam add infiniyield@1
```
```gleam
import infiniyield
pub fn main() {
infiniyield.unfold(2, fn(acc) { yielder.Next(acc, acc * 2) })
|> infiniyield.take(5)
// -> [2, 4, 8, 16, 32]
}
```
Further documentation can be found at <https://hexdocs.pm/infiniyield>.
## Development
```sh
gleam run # Run the project
gleam test # Run the tests
```

11
gleam.toml Normal file
View file

@ -0,0 +1,11 @@
name = "infiniyield"
version = "1.0.0"
description = "Infinitely unfold values on-demand from a function"
licences = ["Apache-2.0"]
repository = { type = "forgejo", host = "git.7cs.dev", user = "lily", repo = "infiniyield" }
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

11
manifest.toml Normal file
View file

@ -0,0 +1,11 @@
# This file was generated by Gleam
# You typically do not need to edit this file
packages = [
{ name = "gleam_stdlib", version = "0.62.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "0080706D3A5A9A36C40C68481D1D231D243AF602E6D2A2BE67BA8F8F4DFF45EC" },
{ name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" },
]
[requirements]
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }

960
src/infiniyield.gleam Normal file
View file

@ -0,0 +1,960 @@
import gleam/list
// Internal private representation of a Yielder
type Action(element) {
Continue(element, fn() -> Action(element))
}
/// An yielder is a lazily evaluated infinite sequence of elements.
///
/// As a lazy data structure no work is done when an yielder is filtered,
/// mapped, etc, instead a new yielder is returned with these transformations
/// applied to the stream. Once the stream has all the required transformations
/// applied it can be evaluated using functions such as `take` and `step`.
///
pub opaque type Yielder(element) {
Yielder(continuation: fn() -> Action(element))
}
// Public API for iteration
pub type Step(element, accumulator) {
Next(element: element, accumulator: accumulator)
}
/// Creates an yielder from a given function and accumulator.
///
/// The function is called on the accumulator and returns `Next` which contains a
/// new element and accumulator. The element is yielded by the yielder and the
/// new accumulator is used with the function to compute the next element in
/// the sequence.
///
/// ## Examples
///
/// ```gleam
/// unfold(from: 5, with: fn(n) {
/// case n {
/// n -> Next(element: n, accumulator: n - 1)
/// }
/// })
/// |> take(5)
/// // -> [5, 4, 3, 2, 1]
/// ```
///
pub fn unfold(
from initial: acc,
with f: fn(acc) -> Step(element, acc),
) -> Yielder(element) {
initial
|> unfold_loop(f)
|> Yielder
}
fn unfold_loop(
initial: acc,
f: fn(acc) -> Step(element, acc),
) -> fn() -> Action(element) {
fn() {
let Next(x, acc) = f(initial)
Continue(x, unfold_loop(acc, f))
}
}
/// Creates an yielder that yields values created by calling a given function
/// repeatedly.
///
/// ```gleam
/// repeatedly(fn() { 7 })
/// |> take(3)
/// // -> [7, 7, 7]
/// ```
///
pub fn repeatedly(f: fn() -> element) -> Yielder(element) {
unfold(Nil, fn(_) { Next(f(), Nil) })
}
/// Creates an yielder that returns the same value infinitely.
///
/// ## Examples
///
/// ```gleam
/// repeat(10)
/// |> take(4)
/// // -> [10, 10, 10, 10]
/// ```
///
pub fn repeat(x: element) -> Yielder(element) {
repeatedly(fn() { x })
}
/// Creates an yielder from an existing yielder
/// and a stateful function that may short-circuit.
///
/// `f` takes arguments `acc` for current state and `el` for current element from underlying yielder,
/// and returns `Next` with the yielded element and new state value.
///
/// ## Examples
///
/// Approximate implementation of `index` in terms of `transform`:
///
/// ```gleam
/// cycle(["a", "b", "c"])
/// |> transform(0, fn(i, el) { Next(#(i, el), i + 1) })
/// |> take(3)
/// // -> [#(0, "a"), #(1, "b"), #(2, "c")]
/// ```
///
pub fn transform(
over yielder: Yielder(a),
from initial: acc,
with f: fn(acc, a) -> Step(b, acc),
) -> Yielder(b) {
transform_loop(yielder.continuation, initial, f)
|> Yielder
}
fn transform_loop(
continuation: fn() -> Action(a),
state: acc,
f: fn(acc, a) -> Step(b, acc),
) -> fn() -> Action(b) {
fn() {
let Continue(el, next) = continuation()
let Next(yield, next_state) = f(state, el)
Continue(yield, transform_loop(next, next_state, f))
}
}
/// Eagerly accesses the first value of an yielder, returning a `Next`
/// that contains the first value and the rest of the yielder.
///
/// ## Examples
///
/// ```gleam
/// let Next(first, rest) = cycle([1, 2, 3, 4]) |> step
///
/// first
/// // -> 1
///
/// rest |> take(3)
/// // -> [2, 3, 4]
/// ```
///
pub fn step(yielder: Yielder(e)) -> Step(e, Yielder(e)) {
let Continue(e, a) = yielder.continuation()
Next(e, Yielder(a))
}
/// Creates an yielder that only yields the first `desired` elements.
///
/// If the yielder does not have enough elements all of them are yielded.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3, 4, 5])
/// |> take(3)
/// // -> [1, 2, 3]
/// ```
///
/// ```gleam
/// cycle([1, 2])
/// |> take(3)
/// // -> [1, 2, 1]
/// ```
///
pub fn take(yielder: Yielder(e), desired: Int) -> List(e) {
yielder.continuation
|> take_loop(desired, [])
}
fn take_loop(
continuation: fn() -> Action(e),
desired: Int,
result: List(e),
) -> List(e) {
case desired > 0 {
False -> list.reverse(result)
True -> {
let Continue(e, next) = continuation()
take_loop(next, desired - 1, [e, ..result])
}
}
}
/// Evaluates and discards the first N elements in an yielder, returning a new
/// yielder.
///
/// This function does not evaluate the elements of the yielder, the
/// computation is performed when the yielder is later run.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3, 4, 5])
/// |> drop(3)
/// |> take(3)
/// // -> [4, 5]
/// ```
///
pub fn drop(yielder: Yielder(e), desired: Int) -> Yielder(e) {
fn() { drop_loop(yielder.continuation, desired) }
|> Yielder
}
fn drop_loop(continuation: fn() -> Action(e), desired: Int) -> Action(e) {
let Continue(e, next) = continuation()
case desired > 0 {
True -> drop_loop(next, desired - 1)
False -> Continue(e, next)
}
}
/// Creates an yielder from an existing yielder and a transformation function.
///
/// Each element in the new yielder will be the result of calling the given
/// function on the elements in the given yielder.
///
/// This function does not evaluate the elements of the yielder, the
/// computation is performed when the yielder is later run.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3])
/// |> map(fn(x) { x * 2 })
/// |> take(3)
/// // -> [2, 4, 6]
/// ```
///
pub fn map(over yielder: Yielder(a), with f: fn(a) -> b) -> Yielder(b) {
yielder.continuation
|> map_loop(f)
|> Yielder
}
fn map_loop(continuation: fn() -> Action(a), f: fn(a) -> b) -> fn() -> Action(b) {
fn() {
let Continue(e, continuation) = continuation()
Continue(f(e), map_loop(continuation, f))
}
}
/// Combines two yielders into a single one using the given function.
///
/// If an yielder is longer than the other the extra elements are dropped.
///
/// This function does not evaluate the elements of the two yielders, the
/// computation is performed when the resulting yielder is later run.
///
/// ## Examples
///
/// ```gleam
/// let first = cycle([1, 2, 3])
/// let second = cycle([4, 5, 6])
/// map2(first, second, fn(x, y) { x + y }) |> take(3)
/// // -> [5, 7, 9]
/// ```
///
/// ```gleam
/// let first = cycle([1, 2])
/// let second = cycle(["a", "b", "c"])
/// map2(first, second, fn(i, x) { #(i, x) }) |> take(2)
/// // -> [#(1, "a"), #(2, "b")]
/// ```
///
pub fn map2(
yielder1: Yielder(a),
yielder2: Yielder(b),
with fun: fn(a, b) -> c,
) -> Yielder(c) {
map2_loop(yielder1.continuation, yielder2.continuation, fun)
|> Yielder
}
fn map2_loop(
continuation1: fn() -> Action(a),
continuation2: fn() -> Action(b),
with fun: fn(a, b) -> c,
) -> fn() -> Action(c) {
fn() {
let Continue(a, next_a) = continuation1()
let Continue(b, next_b) = continuation2()
Continue(fun(a, b), map2_loop(next_a, next_b, fun))
}
}
/// Creates an yielder from an existing yielder and a predicate function.
///
/// The new yielder will contain elements from the first yielder for which
/// the given function returns `True`.
///
/// This function does not evaluate the elements of the yielder, the
/// computation is performed when the yielder is later run.
///
/// ## Examples
///
/// ```gleam
/// import gleam/int
///
/// cycle([1, 2, 3, 4])
/// |> filter(int.is_even)
/// |> take(2)
/// // -> [2, 4]
/// ```
///
pub fn filter(
yielder: Yielder(a),
keeping predicate: fn(a) -> Bool,
) -> Yielder(a) {
fn() { filter_loop(yielder.continuation, predicate) }
|> Yielder
}
fn filter_loop(
continuation: fn() -> Action(e),
predicate: fn(e) -> Bool,
) -> Action(e) {
let Continue(e, yielder) = continuation()
case predicate(e) {
True -> Continue(e, fn() { filter_loop(yielder, predicate) })
False -> filter_loop(yielder, predicate)
}
}
/// Creates an yielder from an existing yielder and a transforming predicate function.
///
/// The new yielder will contain elements from the first yielder for which
/// the given function returns `Ok`, transformed to the value inside the `Ok`.
///
/// This function does not evaluate the elements of the yielder, the
/// computation is performed when the yielder is later run.
///
/// ## Examples
///
/// ```gleam
/// import gleam/string
/// import gleam/int
///
/// "a1b2c3d4e5f"
/// |> string.to_graphemes
/// |> cycle
/// |> filter_map(int.parse)
/// |> take(5)
/// // -> [1, 2, 3, 4, 5]
/// ```
///
pub fn filter_map(
yielder: Yielder(a),
keeping_with f: fn(a) -> Result(b, c),
) -> Yielder(b) {
fn() { filter_map_loop(yielder.continuation, f) }
|> Yielder
}
fn filter_map_loop(
continuation: fn() -> Action(a),
f: fn(a) -> Result(b, c),
) -> Action(b) {
let Continue(e, next) = continuation()
case f(e) {
Ok(e) -> Continue(e, fn() { filter_map_loop(next, f) })
Error(_) -> filter_map_loop(next, f)
}
}
/// Creates an yielder that repeats a given list infinitely.
///
/// If an empty list is provided, attempting to yield a value
/// from the yielder will result in an infinite loop.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2]) |> take(6)
/// // -> [1, 2, 1, 2, 1, 2]
/// ```
///
pub fn cycle(list: List(a)) -> Yielder(a) {
fn() { cycle_loop(list, list) } |> Yielder
}
fn cycle_loop(list: List(a), cur: List(a)) -> Action(a) {
case cur {
[] -> cycle_loop(list, list)
[head, ..tail] -> Continue(head, fn() { cycle_loop(list, tail) })
}
}
/// Creates an yielder of ints, starting at a given start int
/// and incrementing by one each time.
///
/// ## Examples
///
/// ```gleam
/// incrementing(from: 1) |> take(5)
/// // -> [1, 2, 3, 4, 5]
/// ```
///
pub fn incrementing(from start: Int) -> Yielder(Int) {
unfold(from: start, with: fn(current) { Next(current, current + 1) })
}
/// Creates an yielder of ints, starting at a given start int
/// and decrementing by one each time.
///
/// ## Examples
///
/// ```gleam
/// incrementing(from: 5) |> take(5)
/// // -> [5, 4, 3, 2, 1]
/// ```
///
pub fn decrementing(from start: Int) -> Yielder(Int) {
unfold(from: start, with: fn(current) { Next(current, current - 1) })
}
/// Finds the first element in a given yielder for which the given function returns
/// `True`.
///
/// ## Examples
///
/// ```gleam
/// find(cycle([1, 2, 3]), fn(x) { x > 2 })
/// // -> Ok(3)
/// ```
///
pub fn find(in haystack: Yielder(a), one_that is_desired: fn(a) -> Bool) -> a {
haystack.continuation
|> find_loop(is_desired)
}
fn find_loop(continuation: fn() -> Action(a), f: fn(a) -> Bool) -> a {
let Continue(e, next) = continuation()
case f(e) {
True -> e
False -> find_loop(next, f)
}
}
/// Finds the first element in a given yielder
/// for which the given function returns `Ok(new_value)`,
/// then returns the `new_value`.
///
/// ## Examples
///
/// ```gleam
/// find_map(cycle(["a", "1", "2"]), int.parse)
/// // -> Ok(1)
/// ```
///
pub fn find_map(
in haystack: Yielder(a),
one_that is_desired: fn(a) -> Result(b, c),
) -> b {
haystack.continuation
|> find_map_loop(is_desired)
}
fn find_map_loop(continuation: fn() -> Action(a), f: fn(a) -> Result(b, c)) -> b {
let Continue(e, next) = continuation()
case f(e) {
Ok(e) -> e
Error(_) -> find_map_loop(next, f)
}
}
/// Wraps values yielded from an yielder with indices, starting from 0.
///
/// ## Examples
///
/// ```gleam
/// cycle(["a", "b", "c"]) |> index |> take(3)
/// // -> [#("a", 0), #("b", 1), #("c", 2)]
/// ```
///
pub fn index(over yielder: Yielder(element)) -> Yielder(#(element, Int)) {
yielder.continuation
|> index_loop(0)
|> Yielder
}
fn index_loop(
continuation: fn() -> Action(element),
next: Int,
) -> fn() -> Action(#(element, Int)) {
fn() {
let Continue(e, continuation) = continuation()
Continue(#(e, next), index_loop(continuation, next + 1))
}
}
/// Creates an yielder that infinitely applies a function to a value.
///
/// ## Examples
///
/// ```gleam
/// iterate(1, fn(n) { n * 3 }) |> take(5)
/// // -> [1, 3, 9, 27, 81]
/// ```
///
pub fn iterate(
from initial: element,
with f: fn(element) -> element,
) -> Yielder(element) {
unfold(initial, fn(element) { Next(element, f(element)) })
}
/// Creates an yielder that yields elements while the predicate returns `True`.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3, 2, 4])
/// |> take_while(satisfying: fn(x) { x < 3 })
/// |> take(2)
/// // -> [1, 2]
/// ```
///
pub fn take_while(
in yielder: Yielder(e),
satisfying predicate: fn(e) -> Bool,
) -> List(e) {
yielder.continuation
|> take_while_loop(predicate, [])
}
fn take_while_loop(
continuation: fn() -> Action(e),
predicate: fn(e) -> Bool,
result: List(e),
) -> List(e) {
let Continue(e, next) = continuation()
case predicate(e) {
False -> list.reverse(result)
True -> take_while_loop(next, predicate, [e, ..result])
}
}
/// Creates an yielder that drops elements while the predicate returns `True`,
/// and then yields the remaining elements.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3, 4, 2, 5])
/// |> drop_while(satisfying: fn(x) { x < 4 })
/// |> take(3)
/// // -> [4, 2, 5]
/// ```
///
pub fn drop_while(
in yielder: Yielder(element),
satisfying predicate: fn(element) -> Bool,
) -> Yielder(element) {
fn() { drop_while_loop(yielder.continuation, predicate) }
|> Yielder
}
fn drop_while_loop(
continuation: fn() -> Action(element),
predicate: fn(element) -> Bool,
) -> Action(element) {
let Continue(e, next) = continuation()
case predicate(e) {
False -> Continue(e, next)
True -> drop_while_loop(next, predicate)
}
}
/// Creates an yielder from an existing yielder and a stateful function.
///
/// Specifically, this behaves like `fold`, but yields intermediate results.
///
/// ## Examples
///
/// ```gleam
/// // Generate a sequence of partial sums
/// cycle([1, 2, 3, 4, 5])
/// |> scan(from: 0, with: fn(acc, el) { acc + el })
/// |> take(5)
/// // -> [1, 3, 6, 10, 15]
/// ```
///
pub fn scan(
over yielder: Yielder(element),
from initial: acc,
with f: fn(acc, element) -> acc,
) -> Yielder(acc) {
yielder.continuation
|> scan_loop(f, initial)
|> Yielder
}
fn scan_loop(
continuation: fn() -> Action(element),
f: fn(acc, element) -> acc,
accumulator: acc,
) -> fn() -> Action(acc) {
fn() {
let Continue(el, next) = continuation()
let accumulated = f(accumulator, el)
Continue(accumulated, scan_loop(next, f, accumulated))
}
}
/// Zips two yielders together, emitting values from both
/// until the shorter one runs out.
///
/// ## Examples
///
/// ```gleam
/// cycle(["a", "b", "c"])
/// |> zip(incrementing(20))
/// |> take(3)
/// // -> [#("a", 20), #("b", 21), #("c", 22)]
/// ```
///
pub fn zip(left: Yielder(a), right: Yielder(b)) -> Yielder(#(a, b)) {
zip_loop(left.continuation, right.continuation)
|> Yielder
}
fn zip_loop(
left: fn() -> Action(a),
right: fn() -> Action(b),
) -> fn() -> Action(#(a, b)) {
fn() {
let Continue(el_left, next_left) = left()
let Continue(el_right, next_right) = right()
Continue(#(el_left, el_right), zip_loop(next_left, next_right))
}
}
// Result of collecting a single chunk by key
type Chunk(element, key) {
AnotherBy(List(element), key, element, fn() -> Action(element))
}
/// Creates an yielder that emits chunks of elements
/// for which `f` returns the same value.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 2, 3, 4, 4, 6, 7, 7])
/// |> chunk(by: fn(n) { n % 2 })
/// |> take(5)
/// // -> [[1], [2, 2], [3], [4, 4, 6], [7, 7, 1]]
/// ```
///
pub fn chunk(
over yielder: Yielder(element),
by f: fn(element) -> key,
) -> Yielder(List(element)) {
fn() {
let Continue(e, next) = yielder.continuation()
chunk_loop(next, f, f(e), e)
}
|> Yielder
}
fn chunk_loop(
continuation: fn() -> Action(element),
f: fn(element) -> key,
previous_key: key,
previous_element: element,
) -> Action(List(element)) {
let AnotherBy(chunk, key, el, next) =
next_chunk(continuation, f, previous_key, [previous_element])
Continue(chunk, fn() { chunk_loop(next, f, key, el) })
}
fn next_chunk(
continuation: fn() -> Action(element),
f: fn(element) -> key,
previous_key: key,
current_chunk: List(element),
) -> Chunk(element, key) {
let Continue(e, next) = continuation()
let key = f(e)
case key == previous_key {
True -> next_chunk(next, f, key, [e, ..current_chunk])
False -> AnotherBy(list.reverse(current_chunk), key, e, next)
}
}
/// Creates an yielder that emits chunks of given size.
///
/// If the last chunk does not have `count` elements, it is yielded
/// as a partial chunk, with less than `count` elements.
///
/// For any `count` less than 1 this function behaves as if it was set to 1.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3, 4, 5, 6])
/// |> sized_chunk(into: 2)
/// |> take(3)
/// // -> [[1, 2], [3, 4], [5, 6]]
/// ```
///
/// ```gleam
/// cycle([1, 2, 3, 4, 5, 6, 7, 8])
/// |> sized_chunk(into: 3)
/// |> take(3)
/// // -> [[1, 2, 3], [4, 5, 6], [7, 8, 1]]
/// ```
///
pub fn sized_chunk(
over yielder: Yielder(element),
into count: Int,
) -> Yielder(List(element)) {
yielder.continuation
|> sized_chunk_loop(count)
|> Yielder
}
fn sized_chunk_loop(
continuation: fn() -> Action(element),
count: Int,
) -> fn() -> Action(List(element)) {
fn() {
let Another(chunk, next_element) = next_sized_chunk(continuation, count, [])
Continue(chunk, sized_chunk_loop(next_element, count))
}
}
// Result of collecting a single sized chunk
type SizedChunk(element) {
Another(List(element), fn() -> Action(element))
}
fn next_sized_chunk(
continuation: fn() -> Action(element),
left: Int,
current_chunk: List(element),
) -> SizedChunk(element) {
let Continue(e, next) = continuation()
let chunk = [e, ..current_chunk]
case left > 1 {
False -> Another(list.reverse(chunk), next)
True -> next_sized_chunk(next, left - 1, chunk)
}
}
/// Creates an yielder that yields the given `elem` element
/// between elements emitted by the underlying yielder.
///
/// ## Examples
///
/// ```gleam
/// cycle([1])
/// |> intersperse(with: 0)
/// |> take(1)
/// // -> [1]
/// ```
///
/// ```gleam
/// cycle([1, 2, 3, 4, 5])
/// |> intersperse(with: 0)
/// |> take(9)
/// // -> [1, 0, 2, 0, 3, 0, 4, 0, 5]
/// ```
///
pub fn intersperse(
over yielder: Yielder(element),
with elem: element,
) -> Yielder(element) {
fn() {
let Continue(e, next) = yielder.continuation()
Continue(e, fn() { intersperse_loop(next, elem) })
}
|> Yielder
}
fn intersperse_loop(
continuation: fn() -> Action(element),
separator: element,
) -> Action(element) {
let Continue(e, next) = continuation()
let next_interspersed = fn() { intersperse_loop(next, separator) }
Continue(separator, fn() { Continue(e, next_interspersed) })
}
/// Creates an yielder that alternates between the two given yielders.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3, 4])
/// |> interleave(cycle([11, 12, 13, 14]))
/// |> take(8)
/// // -> [1, 11, 2, 12, 3, 13, 4, 14]
/// ```
///
/// ```gleam
/// cycle([1, 2, 3, 4])
/// |> interleave(cycle([100]))
/// |> take(8)
/// // -> [1, 100, 2, 100, 3, 100, 4, 100]
/// ```
///
pub fn interleave(
left: Yielder(element),
with right: Yielder(element),
) -> Yielder(element) {
fn() { interleave_loop(left.continuation, right.continuation) }
|> Yielder
}
fn interleave_loop(
current: fn() -> Action(element),
next: fn() -> Action(element),
) -> Action(element) {
let Continue(e, next_other) = current()
Continue(e, fn() { interleave_loop(next, next_other) })
}
/// Reduces a yielder of elements into a single value by calling a given
/// function on each element in turn, using `list.ContinueOrStop` to determine
/// whether or not to keep iterating.
///
/// ## Examples
///
/// ```gleam
/// import gleam/list
///
/// let f = fn(acc, e) {
/// case e {
/// _ if e < 4 -> list.Continue(e + acc)
/// _ -> list.Stop(acc)
/// }
/// }
///
/// cycle([1, 2, 3, 4])
/// |> fold_until(from: 0, with: f)
/// // -> 6
/// ```
///
pub fn fold_until(
over yielder: Yielder(e),
from initial: acc,
with f: fn(acc, e) -> list.ContinueOrStop(acc),
) -> acc {
yielder.continuation
|> fold_until_loop(f, initial)
}
fn fold_until_loop(
continuation: fn() -> Action(e),
f: fn(acc, e) -> list.ContinueOrStop(acc),
accumulator: acc,
) -> acc {
let Continue(elem, next) = continuation()
case f(accumulator, elem) {
list.Continue(accumulator) -> fold_until_loop(next, f, accumulator)
list.Stop(accumulator) -> accumulator
}
}
/// Returns the first element yielded by the given yielder.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3]) |> first
/// // -> 1
/// ```
///
pub fn first(from yielder: Yielder(e)) -> e {
let Continue(e, _) = yielder.continuation()
e
}
/// Returns nth element yielded by the given yielder, where `0` means the first element.
///
/// For any `index` less than `0` this function behaves as if it was set to `0`.
///
/// ## Examples
///
/// ```gleam
/// cycle([1, 2, 3, 4]) |> at(2)
/// // -> 3
/// ```
///
/// ```gleam
/// cycle([1, 2, 3, 4]) |> at(4)
/// // -> 1
/// ```
///
pub fn at(in yielder: Yielder(e), get index: Int) -> e {
yielder
|> drop(index)
|> first
}
/// Transform a yielder into one that calls the given function on each element.
///
/// Like `map`, but always returns `Nil` for each element after calling the given
/// transformation function.
///
/// ## Examples
///
/// ```gleam
/// cycle(["Tom", "Malory", "Louis"]) |> each(io.println) |> take(3)
/// // -> Nil
/// // Tom
/// // Malory
/// // Louis
/// ```
///
pub fn each(over yielder: Yielder(a), with f: fn(a) -> b) -> Yielder(Nil) {
yielder
|> map(fn(a) {
f(a)
Nil
})
}
/// Add a new element to the start of an yielder.
///
/// This function is for use with `use` expressions, to replicate the behaviour
/// of the `yield` keyword found in other languages.
///
/// If you only need to prepend an element and don't require the `use` syntax,
/// use `prepend`.
///
/// ## Examples
///
/// ```gleam
/// let yielder = {
/// use <- yield(1)
/// use <- yield(2)
/// use <- yield(3)
/// cycle([0])
/// }
///
/// yielder |> take(6)
/// // -> [1, 2, 3, 0, 0, 0]
/// ```
///
pub fn yield(element: a, next: fn() -> Yielder(a)) -> Yielder(a) {
Yielder(fn() { Continue(element, fn() { next().continuation() }) })
}
/// Add a new element to the start of an yielder.
///
/// ## Examples
///
/// ```gleam
/// let yielder = cycle([1, 2, 3]) |> prepend(0)
///
/// yielder.take(4)
/// // -> [0, 1, 2, 3]
/// ```
///
pub fn prepend(yielder: Yielder(a), element: a) -> Yielder(a) {
use <- yield(element)
yielder
}

427
test/infiniyield_test.gleam Normal file
View file

@ -0,0 +1,427 @@
import gleam/int
import gleam/list
import gleeunit
import gleeunit/should
import infiniyield.{Next}
pub fn main() {
gleeunit.main()
}
pub fn step_test() {
let testcase = fn(subject) {
let step =
subject
|> infiniyield.cycle
|> infiniyield.step
let assert [h, ..t] = subject
let Next(h2, t2) = step
h
|> should.equal(h2)
t2
|> infiniyield.take(list.length(t))
|> should.equal(t)
}
testcase([1])
testcase([1, 2])
testcase([1, 2, 3])
}
// a |> cycle |> take(n) == a |> list.take(_, n)
pub fn take_test() {
let testcase = fn(n, subject) {
subject
|> infiniyield.cycle
|> infiniyield.take(n)
|> should.equal(list.take(subject, n))
}
testcase(0, [])
testcase(-1, [])
testcase(0, [0])
testcase(1, [0])
testcase(-1, [0])
testcase(0, [0, 1, 2, 3, 4])
testcase(1, [0, 1, 2, 3, 4])
testcase(2, [0, 1, 2, 3, 4])
}
pub fn transform_index_test() {
let f = fn(i, el) { Next(#(i, el), i + 1) }
["a", "b", "c", "d"]
|> infiniyield.cycle
|> infiniyield.transform(0, f)
|> infiniyield.take(4)
|> should.equal([#(0, "a"), #(1, "b"), #(2, "c"), #(3, "d")])
}
pub fn transform_scan_test() {
let f = fn(acc, el) {
let result = acc + el
Next(result, result)
}
[1, 2, 3, 4, 5]
|> infiniyield.cycle
|> infiniyield.transform(0, f)
|> infiniyield.take(5)
|> should.equal([1, 3, 6, 10, 15])
}
// a |> cycle |> map(f) == a |> list.map(_, f)
pub fn map_test() {
let testcase = fn(subject, f) {
subject
|> infiniyield.cycle
|> infiniyield.map(f)
|> infiniyield.take(list.length(subject))
|> should.equal(list.map(subject, f))
}
let f = fn(e) { e * 2 }
testcase([], f)
testcase([1], f)
testcase([1, 2, 3], f)
testcase([1, 2, 3, 4, 5, 6, 7, 8], f)
}
// map2(cycle(a), cycle(b), f) == list.map2(a, b, f)
pub fn map2_test() {
let testcase = fn(one, other, f) {
infiniyield.map2(infiniyield.cycle(one), infiniyield.cycle(other), f)
|> infiniyield.take(int.min(list.length(one), list.length(other)))
|> should.equal(list.map2(one, other, f))
}
let f = fn(a, b) { a / b }
testcase([], [], f)
testcase([], [2, 10, 3], f)
testcase([10], [2, 10, 3], f)
testcase([10, 20], [2, 10, 3], f)
testcase([10, 20, 30], [2, 10, 3], f)
testcase([10, 20, 30], [2, 10], f)
testcase([10, 20, 30], [2], f)
testcase([10, 20, 30], [], f)
}
pub fn map2_is_lazy_test() {
let one = infiniyield.cycle([])
let other = infiniyield.repeatedly(fn() { panic as "unreachable" })
infiniyield.map2(one, other, fn(x, y) { x + y })
|> infiniyield.take(0)
|> should.equal([])
}
// a |> cycle |> filter(f) == a |> list.filter(_, f)
pub fn filter_test() {
let testcase = fn(subject, f) {
let lst = list.filter(subject, f)
subject
|> infiniyield.cycle
|> infiniyield.filter(f)
|> infiniyield.take(list.length(lst))
|> should.equal(lst)
}
let even = fn(x) { x % 2 == 0 }
testcase([], even)
testcase([1], even)
testcase([1, 2], even)
testcase([1, 2, 3], even)
testcase([1, 2, 3, 4], even)
testcase([1, 2, 3, 4, 5], even)
testcase([1, 2, 3, 4, 5, 6], even)
}
pub fn filter_map_test() {
let testcase = fn(subject, f) {
let lst = list.filter_map(subject, f)
subject
|> infiniyield.cycle
|> infiniyield.filter_map(f)
|> infiniyield.take(list.length(lst))
|> should.equal(lst)
}
testcase([], int.parse)
testcase(["1"], int.parse)
testcase(["1", "2", "3"], int.parse)
testcase(["1", "a", "b"], int.parse)
testcase(["l", "2", "3", "a"], int.parse)
testcase(["1", "c", "3", "a", "b"], int.parse)
testcase(["1", "20", "ten", "4", "5", "69"], int.parse)
}
pub fn repeat_test() {
1
|> infiniyield.repeat
|> infiniyield.take(5)
|> should.equal([1, 1, 1, 1, 1])
}
pub fn cycle_test() {
[1, 2, 3]
|> infiniyield.cycle
|> infiniyield.take(9)
|> should.equal([1, 2, 3, 1, 2, 3, 1, 2, 3])
}
pub fn unfold_test() {
infiniyield.unfold(2, fn(acc) { infiniyield.Next(acc, acc * 2) })
|> infiniyield.take(5)
|> should.equal([2, 4, 8, 16, 32])
}
pub fn incrementing_test() {
let testcase = fn(a, expected) {
infiniyield.incrementing(from: a)
|> infiniyield.take(list.length(expected))
|> should.equal(expected)
}
testcase(0, [0])
testcase(1, [1])
testcase(-1, [-1])
testcase(0, [0, 1])
testcase(0, [0, 1, 2, 3, 4, 5])
testcase(1, [1, 2, 3, 4, 5])
}
pub fn deccrementing_test() {
let testcase = fn(a, expected) {
infiniyield.decrementing(from: a)
|> infiniyield.take(list.length(expected))
|> should.equal(expected)
}
testcase(0, [0])
testcase(1, [1])
testcase(-1, [-1])
testcase(0, [0, -1])
testcase(0, [0, -1, -2, -3, -4, -5])
testcase(1, [1, 0, -1, -2, -3, -4, -5])
}
pub fn drop_test() {
infiniyield.incrementing(from: 0)
|> infiniyield.drop(5)
|> infiniyield.take(6)
|> should.equal([5, 6, 7, 8, 9, 10])
}
type Cat {
Cat(id: Int)
}
pub fn find_test() {
infiniyield.incrementing(from: 0)
|> infiniyield.find(fn(e) { e == 5 })
|> should.equal(5)
infiniyield.incrementing(from: 0)
|> infiniyield.find(fn(e) { e > 10 })
|> should.equal(11)
infiniyield.unfold(Cat(id: 1), fn(cat: Cat) {
infiniyield.Next(cat, Cat(id: cat.id + 1))
})
|> infiniyield.find(fn(cat: Cat) { cat.id == 10 })
|> should.equal(Cat(id: 10))
}
pub fn find_map_test() {
infiniyield.incrementing(from: 0)
|> infiniyield.find_map(fn(e) {
case e == 5 {
True -> Ok(e)
False -> Error(Nil)
}
})
|> should.equal(5)
infiniyield.incrementing(from: 0)
|> infiniyield.find_map(fn(e) {
case e > 10 {
True -> Ok(e)
False -> Error(Nil)
}
})
|> should.equal(11)
infiniyield.unfold(Cat(id: 1), fn(cat: Cat) {
infiniyield.Next(cat, Cat(id: cat.id + 1))
})
|> infiniyield.find_map(fn(cat: Cat) {
case cat.id == 10 {
True -> Ok(cat)
False -> Error(Nil)
}
})
|> should.equal(Cat(id: 10))
}
pub fn index_test() {
infiniyield.cycle(["a", "b", "c"])
|> infiniyield.index
|> infiniyield.take(3)
|> should.equal([#("a", 0), #("b", 1), #("c", 2)])
}
pub fn iterate_test() {
fn(x) { x * 3 }
|> infiniyield.iterate(from: 1)
|> infiniyield.take(5)
|> should.equal([1, 3, 9, 27, 81])
}
pub fn take_while_test() {
infiniyield.cycle([1, 2, 3, 2, 4])
|> infiniyield.take_while(satisfying: fn(x) { x < 3 })
|> should.equal([1, 2])
}
pub fn drop_while_test() {
infiniyield.cycle([1, 2, 3, 4, 2, 5])
|> infiniyield.drop_while(satisfying: fn(x) { x < 4 })
|> infiniyield.take(3)
|> should.equal([4, 2, 5])
}
pub fn scan_test() {
infiniyield.cycle([1, 2, 3, 4, 5])
|> infiniyield.scan(from: 0, with: fn(acc, el) { acc + el })
|> infiniyield.take(5)
|> should.equal([1, 3, 6, 10, 15])
}
pub fn zip_test() {
infiniyield.cycle(["a", "b", "c"])
|> infiniyield.zip(infiniyield.incrementing(from: 20))
|> infiniyield.take(3)
|> should.equal([#("a", 20), #("b", 21), #("c", 22)])
}
pub fn chunk_test() {
infiniyield.cycle([1, 2, 2, 3, 4, 4, 6, 7, 7])
|> infiniyield.chunk(by: fn(n) { n % 2 })
|> infiniyield.take(5)
|> should.equal([[1], [2, 2], [3], [4, 4, 6], [7, 7, 1]])
}
pub fn sized_chunk_test() {
infiniyield.cycle([1, 2, 3, 4, 5, 6])
|> infiniyield.sized_chunk(into: 2)
|> infiniyield.take(3)
|> should.equal([[1, 2], [3, 4], [5, 6]])
infiniyield.cycle([1, 2, 3, 4, 5, 6, 7, 8])
|> infiniyield.sized_chunk(into: 3)
|> infiniyield.take(3)
|> should.equal([[1, 2, 3], [4, 5, 6], [7, 8, 1]])
}
pub fn intersperse_test() {
infiniyield.cycle([1])
|> infiniyield.intersperse(with: 0)
|> infiniyield.take(1)
|> should.equal([1])
infiniyield.cycle([1, 2, 3, 4, 5])
|> infiniyield.intersperse(with: 0)
|> infiniyield.take(9)
|> should.equal([1, 0, 2, 0, 3, 0, 4, 0, 5])
}
pub fn interleave_test() {
infiniyield.cycle([1, 2, 3, 4])
|> infiniyield.interleave(with: infiniyield.cycle([11, 12, 13, 14]))
|> infiniyield.take(8)
|> should.equal([1, 11, 2, 12, 3, 13, 4, 14])
infiniyield.cycle([1, 2, 3, 4])
|> infiniyield.interleave(with: infiniyield.cycle([100]))
|> infiniyield.take(8)
|> should.equal([1, 100, 2, 100, 3, 100, 4, 100])
}
// a |> cycle |> fold_until(acc, f) == a |> list.fold_until(acc, f)
pub fn fold_until_test() {
let testcase = fn(subject, acc, f) {
subject
|> infiniyield.cycle()
|> infiniyield.fold_until(acc, f)
|> should.equal(list.fold_until(subject, acc, f))
}
let f = fn(acc, e) {
case e {
_ if e < 6 -> list.Continue([e, ..acc])
_ -> list.Stop(acc)
}
}
testcase([1, 2, 3, 4, 5, 6, 7, 8], [], f)
[1, 2, 3, 4, 5, 6, 7, 8]
|> infiniyield.cycle()
|> infiniyield.fold_until([], f)
|> should.equal([5, 4, 3, 2, 1])
}
pub fn first_test() {
infiniyield.cycle([1, 2, 3])
|> infiniyield.first
|> should.equal(1)
}
pub fn at_test() {
infiniyield.cycle([1, 2, 3, 4])
|> infiniyield.at(2)
|> should.equal(3)
infiniyield.cycle([1, 2, 3, 4])
|> infiniyield.at(4)
|> should.equal(1)
}
pub fn each_test() {
use it <- infiniyield.each(infiniyield.cycle([1]))
it
|> should.equal(1)
}
pub fn yield_test() {
let items = {
use <- infiniyield.yield(1)
use <- infiniyield.yield(2)
use <- infiniyield.yield(3)
infiniyield.repeat(0)
}
items
|> infiniyield.take(6)
|> should.equal([1, 2, 3, 0, 0, 0])
}
pub fn yield_computes_only_necessary_values_test() {
let items = {
use <- infiniyield.yield(1)
use <- infiniyield.yield(2)
use <- infiniyield.yield(3)
panic as "yield computed more values than necessary"
}
items
|> infiniyield.take(3)
|> should.equal([1, 2, 3])
}
pub fn prepend_test() {
infiniyield.cycle([1, 2, 3])
|> infiniyield.prepend(0)
|> infiniyield.take(4)
|> should.equal([0, 1, 2, 3])
}