Iterators
Rust's iterators are a joy to use. They often enable you to replace a dozen lines of imperative code with a single line of declarative code, without obscuring your program's meaning.
This kind of expressiveness seemed like a great fit for GameLisp, so the language comes with an iterator library which is, for the most part, shamelessly copied from Rust. (To be fair, Rust originally copied many of its iterator APIs from Python...)
(let text "Revered. Exalted. Wise.")
(for word in (->> (split text \space) (rev) (map uppercase))
(pr word " ")) ; prints WISE. EXALTED. REVERED.
Iteration
The iter
primitive type is like a Rust Iterator
,
except that it's dynamically typed and heap‑allocated. (Don't panic! GameLisp is smart enough
to reuse an iterator's heap storage when you're done with it, so the allocation is very cheap.)
The simplest way to allocate a new iterator is by calling (iter x)
, where x
is
some kind of collection or sequence. We say that if something can be passed to the iter
function, it belongs to the iterable
abstract type.
The following types are iterable:
- Arrays iterate over their elements.
- Strings iterate over their characters.
- Tables iterate over their entries as
(key value)
pairs. Each pair is a newly-allocated, two-element array.- Alternatively, you can use the
(keys tbl)
function to iterate over a table's keys, or the(values tbl)
function to iterate over its values.
- Alternatively, you can use the
- Coroutines will be discussed in the next chapter.
- Passing an iterator to
(iter x)
is the identity operation - it just returnsx
.
To advance an iterator and return its next item, use the iter-next!
function. If the iterator has no more items, it will return #n
.
However, you can't assume that an iterator is finished just because it's returned #n
- if so,
iteration over the array (1 2 #n 4 5)
would stop after the first two items! Instead, you should
use the function iter-finished?
. If it returns #t
, then the iterator
has no more items, and the previous #n
item should be discarded.
The for
Macro
GameLisp comes with a for
macro, which is very similar to Rust's for
loop. It
takes an iterable, and evaluates its body once for each item produced by the iterable, binding
that item to a pattern. break
and continue
work as expected.
(for element in '(1 2 3 4 5)
(prn (* element 10)))
(for (key value) in table
(ensure (sym? key))
(prn "{key}: {value}"))
for
isn't doing anything special - it just invokes the iter
, iter-next!
and iter-finished?
functions.
Standard Iterators
GameLisp comes with a large library of built-in iterators. Almost all of Rust's standard
iterators are included: enumerate
, zip
, map
,
lines
, and so on. You can take a look at the standard library
documentation for the full list.
Unlike Rust, GameLisp's once
and repeat
iterators can accept
multiple arguments. If you want an empty iterator which won't produce anything, just call
(once)
with no arguments.
rn
counts upwards from one number to another. (rn 5 10)
is equivalent to the Rust
iterator 5 .. 10
, and (rn 8)
is equivalent to 0 .. 8
. If you need an inclusive upper bound,
you can use rni
: (rni -5 5)
is equivalent to -5 ..= 5
.
Because rn
is such a common iterator, we provide the forn
macro to make it more
convenient to use. (forn
should be read as a contraction of for rn
, in the same way that
defn
is a contraction of def fn
.)
(forn (digit 0 10)
(prn digit))
; ...is equivalent to...
(for digit in (rn 0 10)
(prn digit))
Double-Ended Iterators
Some iterators are "double-ended": items can be produced both from their back and from their
front. For example, array and string iterators are double-ended. You can query whether an iterator
is double-ended using the iter-double-ended?
function, and you can
produce items from the back of a double-ended iterator using
iter-next-back!
.
rev
takes a double-ended iterable and reverses it, treating its back as its front
and its front as its back.
Exact-Size Iterators
Some iterators know more about their length than others do. For example, a rn
iterator knows
the exact number of items it will return, but a lines
iterator has no way to predict its item
count in advance.
We don't provide an equivalent to Rust's size_hint()
, because it wouldn't be useful.
GameLisp doesn't provide any way for you to manipulate the capacity of its collections or reserve
memory in advance.
Instead, the len
function can accept an iterator as its argument. If that iterator
knows its exact length, it returns an integer; if it knows itself to be infinite, it returns the
symbol infinite
; and otherwise it returns the symbol unknown
.
(prn (len (rn 5))) ; prints 5
(prn (len (repeat #t))) ; prints infinite
(prn (len (split text \space))) ; prints unknown
There's nothing to prevent an array or string from being mutated during iteration (although this is strongly discouraged). This means that array and string iterators do not know their exact size. Pushing or popping from the end of a deque during iteration will work as expected, but pushing or popping from the start may cause the iterator to behave unpredictably.
Splaying
We've previously mentioned that you can use ..
, an abbreviation for
splay
, to pass all of an array's elements to the array constructor.
(let triad '(x y z))
(prn (arr 'a 'b 'c ..triad 1 2 3)) ; prints (a b c x y z 1 2 3)
The splay operator is actually much more powerful than this. It will accept any iterable, and pass all of its items as arguments to any function call.
This means that there's no need for GameLisp to have a collect
function: you can just splay
an iterator while calling arr
, str
, tab
, or any
other constructor function.
(prn (str ..(take 5 (repeat \A)))) ; prints AAAAA
There's also no need for GameLisp to have apply
: you
can just splay an array as a function's last argument instead.
If you want to take the sum of an array of numbers, there's no need to look up the API for fold
.
Addition is a variadic function, so you can just call (+ ..the-array)
. The smallest element of
a collection is (min ..coll)
. To test whether an array of numbers is sorted, call
(<= ..numbers)
. Appending the contents of one array onto another is just (push! arr0 ..arr1)
.
Indexing
Arrays, strings, objects and classes are normally indexed using an integer or a symbol. However, it's possible to index them using an iterable instead.
This returns a new iterator which takes each item in turn from the original iterable, indexes
the collection using [coll item]
, and produces the resulting element.
In effect, [coll iterable]
is equivalent to (map (fn1 [coll _]) iterable)
.
; re-order an array's elements
(let shuf (arr ..[src-arr '(1 3 0 2)]))
; equivalent to...
(let shuf (arr [src-arr 1] [src-arr 3] [src-arr 0] [src-arr 2]))
; swizzle an object's fields
(let offset (Vec3 x y z))
(let swizzled (Vec3 ..[offset '(y z x)]))
; discard every second character in a string
(let text "You're filled with DETERMINATION.")
(prn ..[text (step-by 2 (rn (len text)))]) ; prints Yur ildwt EEMNTO.
; use multiple object fields as consecutive function arguments
(draw-sprite spr ..[very-long-coordinates-name '(x y)])
; equivalent to...
(draw-sprite spr [very-long-coordinates-name 'x] [very-long-coordinates-name 'y])
Note that tables do not support this kind of indexing. This is because table keys can belong
to any primitive type, including iterators, arrays, strings, and so on. If you were to call
[table '(0 0 0)]
, it would be ambiguous whether you were trying to access the key (0 0 0)
once, or trying to access the key 0
three times.
Arrows
Creating a complicated iterator might involve several deeply-nested function calls.
As ever, the arrow macros are the best way to flatten out
a deep call hierarchy. ->>
is often a good choice when working with iterators,
because iterator adapters usually expect an iterator or
iterable as their last argument.
(->> my-array (step-by 3) (map (fn1 (+ _ 10))) enumerate)
; ...is equivalent to...
(enumerate (map (fn1 (+ _ 10)) (step-by 3 my-array)))
The arrow macros include special handling for the splay operator. If you prefix one of the
arrowed function calls with ..
, then the result of the previous function will be splayed.
(->> my-array (step-by 3) (map abs) (filter (fn1 (< _ 10))) ..arr)
; ... is equivalent to...
(arr ..(filter (fn1 (< _ 10)) (map abs (step-by 3 my-array))))