Iteration

Ki iterates using the for-in loop, of which there are two incarnations:

fn main() {
    var russian_authors: array(["Dostoyevsky", "Tolstoy", "Chekhov"])
    var authors: array(["Dostoyevsky", "Kafka", "Vonnegut"])

    for (&author in authors) {
        if (author in russian_authors) {
            echo("русский автор: ${author}")
        }
        else {
            echo("Author: ${author}")
        }
    }

    for (index, author in authors) {
        echo("${index + 1}: ${author}")
    }
}

Sometimes you just want numbers. Ki has the range literal for that:

fn main() {
    for (i in [1..10]) {
        echo("${i}")
    }
}

If you iterate over a resizable container, you need a reference to it:

fn main() {
    var names: dynarray(["Aaron", "Bathsheba", "Cain"])

    # In order to iterate over a resizeable container, you need to take a
    # reference to it.  Because mutable references can't coincide with
    # immutable references, this ensures that the container won't be resized,
    # as that requires a mutable reference.
    for (&name! in &names) {
        name = "Zuul"
    }
}

Iterator Generator Methods

Iterators all support a handful of methods that return a generator over the iterator.

The map method

map calls a function on each element of the iterable.

fn increment(int x): int {
    return(x + 1)
}

fn main() {
    val numbers: array([1, 2, 3, 4, 5, 6, 7, 8, 9])
    val plus_one: numbers.map(increment)
}

The filter method

filter calls a function on each element of the iterable, and if that function returns true, it yields the element. Otherwise it moves on to the next element.

fn is_even(int x): bool {
    return(x % 2 == 0)
}

fn increment(int x) (int) {
    return(x + 1)
}

fn main() {
    val numbers:        array([1, 2, 3, 4, 5, 6, 7, 8, 9])
    val evens:          numbers.filter(is_even)
    val evens_plus_one: evens.map(increment)
}

The reverse method

reverse simply iterates over the list backwards.

fn is_even(int x): bool {
    return(x % 2 == 0)
}

fn increment(int x): bool {
    return(x + 1)
}

fn main() {
    val numbers:        array([1, 2, 3, 4, 5, 6, 7, 8, 9])
    val reversed:       numbers.reverse()
    val evens:          reversed.filter(is_even)
    val evens_plus_one: evens.map(increment)
}

reverse is technically a method of an indexable, not an iterable. That's why it has to go first in the example.

The zip function

fn main() {
    var names: dynarray(["Aaron", "Bathsheba", "Cain"])
    var places: dynarray(["Egypt", "Jerusalem", "Nod"])

    for (&name!, &place! in zip(&names!, &places!)) {
        echo("${name} lived in ${place}")
    }
}

Iterator Processors

Iterators also include four methods that process the iterator and return a scalar (a single value).

The fold method

Of all the iterator processors, fold is the most powerful. The canonical example is summing a series of numbers.

fn main() {
    val numbers: array([1, 2, 3, 4, 5, 6, 7, 8, 9])
    val sum:     numbers.fold(numbers, 0, +) # Returns 45
}

The any method

Calls a function on each element of the iterable and returns true the first time the function returns true. If it reaches the end of the iterable, any returns false. This short-circuits, so any elements in the iterable after the one that returns true won't be processed.

The all method

Calls a function on each element of the iterable and returns false the first time the function returns false. If it reaches the end of the iterable, all returns true.

The none method

Calls a function on each element of the iterable and returns false the first time the function returns true. If it reaches the end of the iterable, none returns true.


To Do:

  • How to define a new type as an iterator.
    • start the iteration
    • get the next value
    • signal the end
    • index modification expression
  • zip and unzip
    • Array literals?
  • Generator expressions as static sequence initializers
    • Optimized by Ki
    • No need for gather in that case