commit 8c5aa8092d117e9549cb552f6eeb477eaddff5ae Author: Lily Rose Date: Sat Aug 9 18:40:17 2025 +1000 Initial commit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7c92c48 --- /dev/null +++ b/.github/workflows/test.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md new file mode 100644 index 0000000..eac10e8 --- /dev/null +++ b/README.md @@ -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 . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..a6576b9 --- /dev/null +++ b/gleam.toml @@ -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" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..0a36839 --- /dev/null +++ b/manifest.toml @@ -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" } diff --git a/src/infiniyield.gleam b/src/infiniyield.gleam new file mode 100644 index 0000000..acbe686 --- /dev/null +++ b/src/infiniyield.gleam @@ -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 +} diff --git a/test/infiniyield_test.gleam b/test/infiniyield_test.gleam new file mode 100644 index 0000000..ce3784d --- /dev/null +++ b/test/infiniyield_test.gleam @@ -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]) +}