Cone Language Reference

Collections

On this page

Note: Not built yet: a Set's union and intersection, and a trait that covers a set's insert and remove; each over a closure, and a source that runs its own loop around the body of an each; for an array, its len, the $- operator, copying and filling a range of elements, comparison and pattern matching; for a slice, the count form of a subslice, $-, a slice made from a reference or a pointer, copying and filling a range, and its ptr. The send operator's list of entries (values, n of x, fill x, pairs and each) is built, after a value and after a construction. Each section below says what else is not built.

The collections package holds the growable collections: List, Dict, OrderedDict, Set, Deque and String, all brought in by import collections use *;. Under them are the language's own arrays, whose size is fixed, and slices, borrowed views of a run of elements. This page also covers what every collection shares: the send operator's entries, walking a collection and the adapters. The Guide teaches them in Collections, List and Dict.

Each collection keeps its elements in one block it gets from its region (the heap's unless it was made in another; Collections placed in a region) through a raw pointer, not in an array reference. A list or a dictionary is constructed with new, its first values after <-: new List[i32](100) <- fill 0 (a collection's contents).

List#

A List[T] is a growable array: pushing onto it makes room as it needs, doubling its block as it fills. xs <- v is a push. An element is read with xs[i], which is a borrow of the element, so it is dereferenced to get the value, and &mut xs[i] borrows it to change.

mod listdemo;

import stdio use *;
import collections use *;

fn main() i32 {
  var xs = List[i32].empty();
  xs.push(3);
  xs <- 4;                       // <- is push
  xs.push(5);
  print <- xs.len();
  print <- "\n";
  print <- *xs[1usize];          // an element is a borrow, so dereference it
  print <- "\n";
  match xs.pop() {
    case let v Some {print <- v; print <- "\n";}
    case is None {print <- "empty\n";}
  }
  match xs.get(7usize) {         // get does not panic
    case let v Some {print <- *v; print <- "\n";}
    case is None {print <- "no seventh\n";}
  }
  var total = 0;
  each x in xs {                 // walks the slice the list lends
    total += *x;
  }
  print <- total;
  print <- "\n";
  let zeros = new List[i32](5) <- fill 0;
  print <- zeros.len();
  print <- "\n";
  0i32;
}

This prints 3, 4, 5, no seventh, 7 and 5, one to a line. peek() (an Option of a borrow of the last value) and get(i) (the same for position i, None past the end) do not panic.

A list is String's twin for any element. list.view() lends its elements as a &Array[T], the slice, and the list folds the methods of Array[T] in as its own (list.len is Array[T]'s). list.viewMut() lends them as a &mut Array[T], and a list reached only by a &uni (a &uni parameter, (&uni list).viewUni() for a local) lends them as a &uni Array[T], viewUni, which a parallel each that changes its items may take as its source; a String's bytes likewise.

list.freeze() consumes the list and moves its elements, uncopied, into a fixed-size owner, a Uni[Array[T]] (Memory).

The rest of a list's methods: List[T].empty() and List[T].withCapacity(n) make one, as new List[T] and new List[T](n) do; capacity(), isEmpty() and reserve(n); set(i, v) and swap(i, j), each a panic past the end; clear(); truncate(n), which cuts the list to n values, finalizing those it drops, and does nothing when n is not less than its length; resize(n, v), which makes it n copies of a value long or drops the values past n, and exists only where T copies; and sortBy(f), which sorts in place by a closure that takes two values and answers a negative number when the first comes first. The cursors (iter, mutItems, indexed, chunks, mutChunks, mutArrays, drain) are under Walking a collection.

Dict#

Dict[K, V] is a SwissTable: an open-addressed table with a control byte for each bucket (empty, removed, or the top 7 bits of the key's hash), read eight at a time as one 64-bit word, so a lookup compares a key only in a bucket whose tag matches; it holds up to 7/8 of its buckets, and doubles when full. Its keys are any type that is Hash: integers and bool, a String, a pool.Ref[T], a struct that declares is Hash (the compiler supplies its hash from its fields, and the struct writes the == that goes with it); a float key is refused. A key moves in and the dictionary owns it.

mod dictdemo;

import stdio use *;
import collections use *;

fn main() i32 {
  var counts = new Dict[String, i64];
  let words = ["to", "be", "or", "not", "to", "be"];
  each w in words {
    *counts.getOrInsert(String.from(*w), 0i64) += 1i64;   // one search; the String moves in
  }
  print <- counts.len();
  print <- "\n";
  print <- *counts["be"];                          // a &str looks up a Dict[String, V]
  print <- "\n";
  if counts.contains("not") { print <- "has not\n"; }
  match counts.get("or") {
    case let n Some {print <- *n; print <- "\n";}
    case is None {print <- "none\n";}
  }
  match counts.remove("to") {                      // the value comes out
    case let n Some {print <- n; print <- "\n";}
    case is None {print <- "none\n";}
  }
  print <- counts.len();
  print <- "\n";
  0i32;
}

This prints 4, 2, has not, 1, 2 and 3, one to a line.

Looking things up#

Lookups take a &K: get(&k) is an Option of a borrow of the value, getMut a mutable one, getCopy an Option of a copy (only where the value copies, which is the lookup that works through a shared Rc[mut, Dict[K, V]]), contains, remove (the value moved out, the key finalized), d[&k] and &mut d[&k] (a panic for a key it does not hold), insert(k, v) (when it replaced an entry, finalizing the old key, an Option holding the old value moved out to the caller, and None for a new key; the value dies with the caller's binding, or at the end of a statement that ignores it; d <- k: v ignores it), getOrInsert(k, v) (a mutable borrow of the value held, or else v moved in, with one search: *counts.getOrInsert(word, 0) += 1), clear, len and capacity (the entries it holds before it grows).

Text keys looked up as &str#

A Dict[String, V], a Dict[Uni[str], V] or a Dict[Rc[str], V] (an owner of text is Hash, an immutable key with no spare capacity; a literal given to insert is copied into the owner) also takes the key as the body it lends or holds, a &str (d.contains("core")), for every one of these lookups: a literal, a view (s.view(), a slice of text), a Uni[str] and an Rc[str] are each lent as a &str, so none builds a String to ask. They find the key they equal because a str, a String and a literal feed a hasher the same bytes (str has a hash of its own) and == compares the bytes. A d["sdl"] of a key it does not hold is a panic that names the key; one by a &K does not. A &Array[u8] is not text until it says so: bytes as &str.

Visiting order and seeds#

A dictionary is unordered: each dictionary keeps a seed of its own, the process's random one by default, so keys(), values() and entries() (cursors, each next() giving an Option of a borrow, the dictionary loaned while one is used) visit in a different order on every run, and a program sorts, or looks a key up, rather than rely on it. Dict[K, V].deterministic() fixes the seed, and then the same keys inserted in the same order are visited in the same order on every run; Dict[K, V].withSeed(n) takes one of your own. When the order matters to the output, use an OrderedDict, a Dict that visits in the order of insertion, with the same methods under the same names.

OrderedDict#

OrderedDict[K, V] is a Dict that visits in the order of insertion, on every run and under every seed, which is what a program wants to print, write out as a file, or replay (a JSON object, a configuration, HTTP headers). It has the same methods under the same names, including the &str lookups of an OrderedDict[String, V] and the cursors keys(), values() and entries(), and the same seeds.

mod ordered;

import stdio use *;
import collections use *;

fn main() i32 {
  var headers = new OrderedDict[String, String];
  headers <- String.from("Host"): String.from("example.org");
  headers <- String.from("Accept"): String.from("text/html");
  headers <- String.from("Host"): String.from("cone.dev");   // replaced where it stands
  each key, value in headers {
    print <- key.view();
    print <- ": ";
    print <- value.view();
    print <- "\n";
  }
  headers.remove("Host");                                    // the later entries move down
  headers <- String.from("Host"): String.from("again");      // a key put back goes last
  each key in headers.keys() {
    print <- key.view();
    print <- "\n";
  }
  0i32;
}

This prints Host: cone.dev, Accept: text/html, then Accept and Host.

Its layout is a compact, ordered table: a dense array of (hash, key, value) in insertion order, which the cursors walk front to back, and beside it an index with the same control bytes and eight-at-a-time probe as Dict's, holding for each bucket a u32 position in the array, so a table holds fewer than 232 entries. A lookup is one memory access slower than Dict's; iteration is faster, with no empty buckets to skip. Inserting a key it holds replaces that entry where it stands; a key put back after a remove goes to the end. remove keeps the order: every later entry moves down one place, so it costs the entries after the one removed. swapRemove(&k) is O(1) but moves the last entry into the hole, which breaks the order. If you mostly look keys up and rarely iterate, Dict is the faster choice.

Set#

Set[T] is a set of any type that is Hash: a Dict whose values take no bytes, so it has the dictionary's table, seed and unordered visiting. insert(x) answers whether x was new (a held element stays, and the item given dies), contains(&x), remove(&x) (true when it was held), clear, len, isEmpty, capacity, deterministic() and withSeed(n); items() is a cursor over borrows of the elements. A Set[String] takes an element as the &str a String lends (s.contains("core")).

mod setdemo;

import stdio use *;
import collections use *;

fn main() i32 {
  var seen = new Set[String];
  let words = ["to", "be", "or", "not", "to", "be"];
  var repeats = 0;
  each w in words {
    if not seen.insert(String.from(*w)) {      // insert answers whether the element was new
      repeats += 1;
    }
  }
  print <- seen.len();
  print <- " distinct, ";
  print <- repeats;
  print <- " repeats\n";
  if seen.contains("or") { print <- "or is in\n"; }
  if seen.remove("or") { print <- "or removed\n"; }
  if not seen.contains("or") { print <- "or is out\n"; }
  0i32;
}

This prints 4 distinct, 2 repeats, then or is in, or removed and or is out.

Deque#

Deque[T] is a double-ended queue, a growable ring buffer: its values are in one block, as a circle, the first at a slot that moves, so push and pop at the back and pushFront and popFront at the front are all O(1), where a List can take a value off its front only by moving every other one. peek() and peekFront() are an Option of a borrow of the last and the first value, get(i) the same of position i counted from the front (None past the end), d[i] and &mut d[i] borrow it (a panic past the end), and items() is a cursor over borrows from the front to the back.

mod dequedemo;

import stdio use *;
import collections use *;

fn main() i32 {
  var line = Deque[i32].empty();
  line.push(1);
  line.push(2);
  line.push(3);
  line.pushFront(0);                  // O(1) at the front too
  match line.popFront() {
    case let v Some {print <- v; print <- "\n";}
    case is None {print <- "empty\n";}
  }
  match line.peek() {                 // the last, as a borrow
    case let v Some {print <- *v; print <- "\n";}
    case is None {print <- "empty\n";}
  }
  print <- *line[1usize];             // counted from the front
  print <- "\n";
  each v in line {
    print <- *v;
    print <- " ";
  }
  print <- "\n";
  0i32;
}

This prints 0, 3, 2 and 1 2 3.

A full block is replaced by one twice the size (4, 8, 16, ...), the values moved in order with the first in slot 0; it never shrinks. reserve(n), capacity, clear (finalizing front to back, keeping the block), len, isEmpty and new Deque[T](n), a block of exactly n slots, are a list's. Its values may wrap round the block's end, so it has no view(): read them by position or with the cursor.

String#

String is the growable string: a list of bytes, lent as text, &str, by s.view(), with the methods of text folded in as its own. The Guide's String teaches it, and its methods are listed with text's.

The send operator's entries#

<- is the one operator for giving values to anything that takes them: print <- "Hello" writes text out, xs <- 4 pushes onto a list, ages <- name: 31 puts a key and its value in a dictionary, and new List[i32] <- 2, 3, 5 builds a list with its contents. A type takes values this way by declaring methods that overload `<-` (Method patterns).

The receiver is borrowed for the append with the permission that the chosen method's self declares. A method taking self &mut, as a collection's does, needs a receiver that may be written: a var variable or global. Held through a reference, it is the reference's permission that is checked, never the variable's, so a let variable holding an Rc[mut, List[i32]], an Arc[mut, List[i32]] or a &mut List[i32] is appended to, while an Arc[imm, ...], a &List[i32] or a list in a let variable is refused. A method taking self & changes nothing the receiver holds, as the methods of stdio's print do, whose data lives in the C library's stream: it is available on a receiver in a let variable, and print is a let global. (Borrowed references explains borrowing.)

What follows the send operator is a list of entries, separated by commas, and each is appended in turn, as though it had an append of its own:

print <- "Hello ", user;   // equivalent to: print <- "Hello "; print <- user

Besides a value, an entry may take one of four forms, each recognized only in this list:

  • n of x appends n values, evaluating x once for each: xs <- 4 of 0. Nothing is shared between them, so a construction gives a new value each time, while a variable holding a value that moves is moved by the first and is gone for the second. The count is a usize; a constant one is from 0 through 4294967295, as an array's size is.
  • fill x appends x, evaluated once for each, until the collection is full: until its len() reaches its capacity(), the two methods it asks. A collection that declares neither cannot be filled.
  • k: v appends a key and its value together, through a <- method that takes the two: ages <- name: 31. Appended in order, a later value for a key the dictionary already holds replaces the earlier.
  • each draws values from a source, one at a time: each src appends every item of src (an item a loop would lend as a borrow is appended as a copy of it), and each x in src if cond yield v is a loop's own header (below), the yield giving what is appended on each pass. yield k: v appends a pair. The source may be anything each walks, a number range included. With parallel before the each the passes run at the same time and the values are joined in the order of the passes, into a List only (The parallel builder).
var xs = new List[i32];
xs <- 1, 2, 3 of 0;          // 1, 2, 0, 0, 0
xs <- fill -1;                // -1 until xs.len() is xs.capacity()
tx <- job, 2 of next();       // a channel's '<-' sends each
let copy = new List[i32] <- each xs;                          // every item of xs
let evens = new List[i32] <- each x in xs if x % 2 == 0 yield x * 10;
let older = new Dict[i32, i32] <- each id, age in ages yield *id: age + 1;
let tens = new List[i32] <- each xs.iter().filter(x => *x > 2).map(x => *x * 10);   // a chain of adapters
xs <- 9, each other.drain(), 8;      // items that move are handed over, not copied
let doubled = new List[i32] <- parallel each x in xs yield x * 2;   // at the same time, in order

The loop reads its source while the list is appended to, so the two may not be the same list: xs <- each xs is refused as each x in xs { xs.push(*x); } is, since the list could move what the loop is reading. An array's contents may not use each: its size is fixed, and a loop's count is not known when it is compiled.

The words of and fill mean this only in the list; elsewhere they are ordinary names. In the list, fill is the word when a space and then a value follow it, so fill(3) still calls a function and xs <- fill; still appends a variable named fill.

The list runs to the end of the statement, across as many lines as it needs. Inside another comma-separated list, such as a call's arguments, a construction's contents (below) take one entry, and several are parenthesized: draw(new List[i32] <- (1, 2), x).

Inside a with block, <- with nothing on its left appends to this (The send operator with nothing before it). A negative value after a comparison pattern's < needs a space, case < -1, since <- is this operator.

A collection's contents#

A construction may be followed by <- and a list. The value is built by its init, whose arguments the parentheses carry, such as a capacity; then each entry is appended to it; and the whole is the value built:

let primes = new List[i32] <- 2, 3, 5, 7;
let zeros = new List[f32](100) <- fill 0.0;     // room for 100, then filled
let ages = new Dict[String, i32] <- String.from("Ann"): 31, String.from("Bo"): 12;

An array has no init and no <-, and its size is fixed, so its construction is its contents, which fill it exactly: new Array[f32, 4] <- fill 0.0. Written with several sizes, its entries are its scalars, row by row: new Array[i32, 2, 3] <- 1, 2, 3, 4, 5, 6.

An allocation is a construction too, and takes contents the same way. An array's fill it in the region's memory, an element at a time; a collection's are appended through the new reference:

let grid = new Rc[mut, Array[f32, 4]] <- fill 0.0;
let names = new Rc[mut, List[i32]](10) <- 1, 2, 3;

each inside <-: <- each … if … yield#

An each may stand in the list after the send operator (above), beside values, of, fill and pairs. With only a source it drains it: every item is appended. With variables it is the loop's own header, including the if filter, and a yield in the place of a block says what each pass appends:

let copy = new List[i32] <- each other;                     // every item of other
let big = new List[i32] <- each x in other if x > 9 yield *x;
let squares = new List[i32] <- each i in 1 .. 5 yield i * i;    // 1 4 9 16 25
let index = new Dict[i32, str] <- each p in people yield p.id: p.name;
xs <- 0, each x in other yield x + 1, 99;               // in the middle of a list
let tens = new List[i32] <- each other.iter().filter(x => *x > 2).map(x => *x * 10);   // a chain

This yield belongs to the entry: it is not a generator's, which is a statement of its own, so a generator may build a list this way and then hand values to its caller. Two or more variables unpack a tuple item as they do in a loop, and yield k: v appends a pair to a collection that takes a key and a value. The items an each gives of a collection are borrows, so a drain appends a copy of each (a type that moves is not copied: drain a source that hands its items over, each other.drain()), and a yield writes the * itself where it wants the value. The list being appended to cannot be the source: xs <- each xs is refused, as pushing to a list while a loop walks it is. An array's contents cannot hold an each, as its size is fixed.

The source may be a chain of adapters, in the bare form or after in in the header form: the chain is a cursor, and the entry walks it as any other, one loop. A chain that starts at a list's iter() hands out borrows, so the items are copied as from the list itself; a chain whose closure makes the value (map(s => String.from(s.view()))) hands the value over; and one that starts at drain() moves the items out of the list, those a filter rejects are dropped, and the list is left empty:

let lengths = new List[usize] <- each names.drain().map(s => s.len);      // names is left empty
let kept = new List[i32] <- each v in xs.iter().map(x => *x * 2) if v > 6 yield v + 1;
let squares = new Dict[i32, i32] <- each k, v in xs.iter().map(x => (*x, *x * *x)) yield k: v;

A chain that starts at the list being appended to is refused as xs <- each xs is (xs <- each xs.iter().map(f), and with drain()). A chain whose closure reads the list while it grows (its len, an item's value) is allowed. A dictionary takes pairs by yield k: v in the header form; the bare form new Dict[K, V] <- each pairs is not built for any source. new List[T] <- parallel each chain is refused: a chain cannot be split (on its own page).

A cursor whose next names no lifetime lends: each item is a borrow of the cursor itself, gone at the next step. A chain over one feeds the entry the same way when each entry is a value made from the item (a number copied, map(x => *x + 1), a field read). A list that would keep the lent items themselves (of a struct holding the borrow) is refused, whatever adapters sit in the chain.

Walking a collection#

Walking a collection is each. A List is walked as the slice it lends, a counted loop; iter() is its cursor, for code generic over cursors (Iterator), and the methods of Array[T] it folds in come with it: mutItems() lends each element as a &mut to change, indexed() pairs a position (a usize) with a borrow of the element, chunks(n) and mutChunks(n) hand out its values in runs of n (each run a slice, a &mut one from mutChunks, which a list gives itself), asArrays[N]() and mutArrays[N]() see them as rows of N over the same block (Seen as Rows), and drain() leaves the list empty at once (keeping its block) and hands each value over by value, finalizing those a loop left early did not reach.

mod walking;

import stdio use *;
import collections use *;

fn main() i32 {
  var xs = List[i32].empty();
  xs <- 1, 2, 3, 4, 5;

  each x in xs.mutItems() {              // each element lent as a &mut
    *x = *x * 10;
  }
  each i, x in xs.indexed() {            // a position beside a borrow of the element
    print <- i;
    print <- "=";
    print <- *x;
    print <- " ";
  }
  print <- "\n";
  each run in xs.chunks(2usize) {        // runs of two; each run is a slice
    print <- run.len;
    print <- " ";
  }
  print <- "\n";
  var ys = List[i32].empty();
  ys <- 7, 8, 9;
  each a, b in zip(&xs, &ys) {           // two collections side by side
    print <- *a + *b;
    print <- " ";
  }
  print <- "\n";
  each x in xs.drain() {                 // leaves the list empty; each value by value
    print <- x;
    print <- " ";
  }
  print <- "\n";
  print <- xs.len();
  print <- "\n";
  0i32;
}

This prints 0=10 1=20 2=30 3=40 4=50, 2 2 1, 17 28 39, 10 20 30 40 50 and 0, each on its own line.

A Deque, a Set, a Dict and an OrderedDict each have an iter() that each calls itself: a deque's and a set's give a borrow of each element (items() is the same cursor), a dictionary's gives its pairs, a borrow of the key and a borrow of the value, which each key, value in dict unpacks. The dictionaries' other views are cursors too: keys(), values() and entries() (a borrow of each entry, with its key and value). zip(&a, &b) walks two lists, arrays, slices, deques, sets or dictionaries side by side. A dictionary or set is not changed while a loop walks it, and a dictionary's order is the one described above. A String's text is walked through chars() and charIndices() (The Methods of str).

Note: Not built yet: closure iterators, and a source that runs its own loop around the body of an each.

What each walks#

each supports several ways to iterate over a sequence of values:

  • Number range, for simple arithmetic progressions.
  • Array, slice or list, to work with individual elements.
  • Cursor, something with a next method, and anything that gives one. A generator is a cursor.
  • Closure, to deal with algorithmically-generated values (not built yet).

Arrays, slices and lists#

each can be used to iterate through every element of an array, a slice or a list, one at a time. The variable is a borrow of the element (the elements themselves stay where they are, and the list is still whole afterwards). A borrowed number needs no * in an expression: it is read through on either side of an operator (x + total, total + x), on the right of a compound assignment (sum += x) and in a comparison (x > 1, or x == y for two borrows, which compares the values), and so is any value that copies freely: a bool, a char, a plain struct (Implicit De-referencing). The * is written where the value itself is wanted: a copy of the item (let y = *x), a value handed to a function, a break or yield, or a store through a &mut:

var sum = 0;
each x in [4, 3, 8, 2] {
  sum += x;
}

var chars = 0usize;
each name in names {      // a List[String]: name is a &String
  chars += name.len;
}

An array or a slice is walked by a counted loop over its length, and so is a list, which lends its elements as a slice: none of them makes an iterator object, and the loop is the one a while over the indexes would be. While the loop runs the list may not be changed, since the borrows it hands out would not survive a change. For the same reason an await in the body is refused over an array or a list, whose borrow would have to last across it; over a number range it is fine (An await in a loop).

Changing items in place: mutItems#

A plain each only reads. To change the items of a list, an array or a slice where they are, walk mutItems(), which lends each item as a &mut:

each x in list.mutItems() {
  *x = *x * 2;       // x is a &mut of the item
}

It is the same counted loop as a plain each over a slice. The list stays borrowed mutably while the loop runs, so nothing else may read or change it until the loop is over. A list, an array or a slice that is only read has no mutItems to take (a list in a let variable, or a &Array[T] parameter).

A borrow written as the source, each x in &mut list, is refused, in parallel each and in <- each too, with the message to write list.mutItems(): each reads what it walks, and the change in place is a method that lends the items. each x in &list is allowed and reads, as each x in list does.

Taking items out: drain#

list.drain() gives the values up one at a time, moving each out of the list into the loop's variable, which then holds the value itself (not a borrow of it), so it works for items such as a String that cannot be copied:

each name in names.drain() {   // name is a String, moved out of names
  joined.append(name.view());
}
// names is now empty (its block is kept for reuse)

The list is left empty as soon as drain is called, and may not be used while the loop runs. Each pass's value is finalized as the pass ends. A loop left early, by break or return, finalizes the values it did not reach.

Positions: indexed#

indexed() of a list, an array or a slice gives each position with a borrow of its item, a pair that two variables unpack. Walked directly by each, it is a counted loop, as a plain each is:

each i, name in names.indexed() {   // i is a usize, name a borrow of the item
  print(i, name);
}

A bare indexed() always borrows its items for reading, on a &mut slice as on any other. To change them, call it on mutItems(), a cursor of its own whose indexed() lends each item as a &mut:

each i, p in buf.mutItems().indexed() {
  *p = f(i);                        // p is a &mut of the item
}

Every other cursor that declares is Iterator[T] has an indexed() too: a generator, a chain, a cursor of one's own. Its item is the same pair, the position (a usize, counted from 0 as the cursor is walked) and what the cursor gives. Positions count what the cursor wrapped gives, so after a filter they number what is left, and an indexed() before the filter keeps the original positions. A cursor that lends its items (one whose next names no lifetime, so an item is gone at the next step) still lends once numbered: the borrow is inside the pair, and every use that would keep it past the next step is refused as it is without the numbering.

each i, y in xs.iter().map(x => *x * 2).filter(y => *y > 20).indexed() {
  printInt(i64.from(i) * y);        // i counts the items the filter kept
}

Runs: chunks and mutChunks#

chunks(n) of a list, an array or a slice hands out its items in runs of n, each run a slice; mutChunks(n) hands each out as a &mut slice, to change in place. The last run is what is left, so it is shorter when n does not divide the count; n larger than the count is one run of everything; an empty source has no run. n of 0 is a panic, at the line of the call. indexed() numbers the runs from 0:

each run in list.chunks(4) {              // run is a &Array[T] of 4 items (the last may be fewer)
  ...
}
each y, row in pixels.mutChunks(width).indexed() {
  each x, p in row.mutItems().indexed() { *p = shade(x, y); }
}

The cursor chunks gives has len (how many runs are left), at(k) (run number k of those left) and split(at) (the runs before run at and those from it on, each a cursor over the same items), so it is a ParallelIterable that cuts only between runs. A parallel each walks it.

For a buffer of pixels, three bytes each, mutArrays[3]() first sees it as a slice of Array[u8, 3] (Seen as Rows), so the runs are rows of pixels and a pixel's channels are indexed: each y, line in pixels.mutArrays[3]().mutChunks(width).indexed() { line[x][0] = ...; }

Two together: zip#

zip(&xs, &ys) walks two lists (or deques, sets, dictionaries, or anything else with an iter method) side by side, giving a pair of what each gives, and ends when the shorter does:

each x, y in zip(&xs, &ys) {
  ...
}

An array and a slice have an iter() too (each x in a.iter() is the loop each x in a is), and zip takes them, with the & a list takes: zip(&arr, &xs), or two slice parameters, zip(s, t).

A cursor is zipped with its own method, which takes the other cursor by value (the & of the function would borrow a value that ends with the statement), so a chain goes on from it. Every cursor that declares is Iterator[T] has it (see Adapters), the cursor of a list or an array, a deque, a dictionary, a generator and a cursor of your own, as well as what chunks(n), mutChunks(n) and mutItems() give: xs.iter().zip(ys.iter()), a.mutChunks(n).zip(b.mutChunks(3usize * n)). It gives the same pairs as the function (a tuple, which each x, y unpacks), ends with the shorter, and the pairs can be mapped, filtered and counted like any items: xs.iter().zip(ys.iter()).map(p => *p.0 * *p.1).sum(). The argument is a cursor, not a collection: xs.iter().zip(ys) is refused. Its indexed() numbers the pairs from 0, three variables, the position and one item from each source. each walks it through next.

each y, ra, rb in a.mutChunks(n).zip(b.mutChunks(3usize * n)).indexed() {
  ...                          // ra is a &mut slice of n items of a, rb of 3n items of b
}
each x, y, z in a.mutChunks(n).zip(b.mutChunks(m)).zip(c.mutChunks(k)) {
  ...                          // a third cursor: a flat triple of the three
}

Cursors#

A cursor is anything with a next method that answers an optional value, Some with the next item or None when there are no more. A type needs no declaration to be one, only the method (it is met by shape, as Iterator in the core library names, which is what generic code asks for):

struct Countdown {
  n i32;

  pub fn next(self &mut) Option[i32] {
    if n <= 0 { return None[i32]; }
    let v = n;
    n = n - 1;
    Some[i32](v);
  }
}

var countdown = new Countdown(3);
var digits = 0;
each n in countdown {     // iterates: 3 2 1
  digits = digits * 10 + n;
}

The cursor is used as it is: a cursor in a variable is advanced there, so a loop left with break can be followed by another that carries on from where it stopped. A cursor that lends (a deque's items(), say) gives borrows of its items, as an array does; one that gives values gives the values themselves. An item that moves (a String the cursor hands out) is taken out of the optional value by the one variable, and is the variable's from then on; several variables, which would take the elements of a moving pair out one by one, are refused.

Something that gives a cursor#

A value with no next but an iter method, which gives a cursor, is walked through the cursor it gives, as if the loop had called iter itself:

each x in collection {  // equivalent to: each x in collection.iter()
}

The collections give cursors in this way: a deque's, a set's and the dictionaries'. Calling iter only reads the collection: it is borrowed and not consumed, and may not be changed while the loop runs. Other views of a collection are ordinary methods that answer a cursor, walked the same way: each k in dict.keys(), dict.values(), deque.items(), text.chars() (Collection Types).

Two variables

Two or more variables unpack an item that is a tuple. A dictionary's cursor gives its pairs, a borrow of the key and a borrow of the value, so

var both = 0;
each key, value in ages {   // a Dict[i64, i64]
  both += key * value;
}

and one variable would take the pair whole (pair.0 and pair.1). A number of variables that does not match the tuple, and any number but one for an array, a slice or a list, are refused.

Adapters#

Every cursor that declares itself an Iterator has the steps that work on a sequence as a whole, as methods the trait shares: the cursors of the collections (xs.iter(), chunks, mutItems, indexed, a dictionary's, a deque's), a zip, text.chars(), a generator, and a struct of your own that says is Iterator[T], T being what its next gives. A step gives a cursor again; none does any work until a terminal method walks the chain once, from where the cursor is, and leaves it advanced:

let k = 3;
let total = xs.iter().map(x => *x * k).filter(y => *y > 9).sum();
each v in xs.iter().map(x => *x * k) {      // a step is a cursor too: each walks it
}

struct Countdown is Iterator[i64] {         // declared: it has map, filter, sum and the rest
  n i64;
  pub fn next(self &mut) Option[i64] { ... }
}
map(f)each item through f; the closure is typed from the item and its result is the new item's type
filter(f)the items f is true of; f is given a borrow of the item
take(n), skip(n)at most the first n items; all but the first n (any integer, a plain literal included; more than there are is none)
zip(other)pairs of an item from this cursor and one from other (a cursor), ending with the shorter (Two together)
fold(init, f)f(result so far, item) over the items, starting from init
any(f), all(f)whether f is true of some / every item; stops at the first that settles it; false / true for no items
count()how many items
find(f)the first item f (given a borrow) is true of, as an Option
sum(), min(), max()for number items: the total (0 for none); the smallest and largest as an Option, None for no items (the first of equals)

An item is whatever the cursor gives. A cursor that lends (xs.iter()) gives borrows, so a closure reads through them: map(x => *x * 2). A filter's closure is given a borrow of the item, a borrow of a borrow for such a cursor, which an operator reads through (filter(y => *y > 3) for either kind). sum, min and max are methods only where the items are numbers (integers, f32, f64), so a cursor that lends is mapped to values first, map(x => *x).max(), and a struct has none of them. find hands the item back, and a borrow cannot be handed back through the chain's steps (each step's next is a method whose signature names no lifetime, so its result is held to lend from the step), so it too is called on values: map(x => *x).find(y => *y == 5).

A chain is compiled into one loop, with each closure's body in line. How that works, and the Iterator trait that gives a cursor its adapters, are in Traits and Generativity.

Not built yet: a source that runs its own loop around the body of an each, taking the body as a closure, for a source that cannot hand out its items one by one (a line reader that lends each line from its own buffer, say). each would use it when the source has it, and otherwise use next; break, continue and return in the body would reach the source as signals, and an await in such a body may be refused. The parallel reductions are on their own page. A chain feeding the entries of a collection being built (<- each) is built.

Array#

Note: Implemented: the array type, with one size or several, array literals, nested arrays, elements given by expressions, a string as an array of u8, construction with contents (new Array[T, n] <- fill x), copying, getting and setting one element by index, each over an array, and a run-time bounds check on every index. Not implemented: the len method (a slice's is), the $- operator, multi-element segments and fills, comparison, and pattern matching. An index out of range is caught only at run time, even when it is a constant.

A list grows. An array's size is fixed when it is made, and never changes. That suits values whose number is part of the problem: the three coordinates of a point in space, the sixteen numbers of a 4×4 matrix, a table of the days in each month. An array is written as its values in square brackets, and is read with [i] and walked with each, just as a list is:

let days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
var total = 0;
each d in days {
  total += d;
}
print <- total, " days; February has ", days[1], "\n";   // 365 days; February has 28

let point = [3d, 4d, 12d];
let far = (point[0] * point[0] + point[1] * point[1] + point[2] * point[2]).sqrt();   // 13

var grid = new Array[f64, 4, 4] <- fill 0d;     // a 4×4 matrix of zeros
each i in 0usize ..< 4 {
  grid[i][i] = 1d;                              // ones down the diagonal
}

The size is part of an array's type: days is an Array[i32, 12]. So an array takes no <- once it is made, though fill can fill a new one.

An array holds a specified number of values, all of the same type. The size of the array (its number of elements) is a fixed, constant value and is not resizable. The array's elements are located next to each other in memory. Elements are individually accessed using an unsigned integer index.

For example:

var numbers Array[f32, 3] = [3.1, 4.1, 5.92];
let nbr = numbers[1];     // nbr is 4.1
numbers[0] = 2.;          // numbers is now [2., 4.1, 5.92]

Here, numbers is an array containing three floating-point numbers as its elements. As the redundant type specification Array[f32, 3] indicates, the type of its elements is f32 and the length of the array is 3. The [] operator enables access to a specific element.

The array type#

An array type is written Array[T, n]: the element type first, then the size. The size is an integer literal from 0 through 4294967295, and is part of the type: an Array[f32, 3] and an Array[f32, 4] are different types. Array looks like a generic type taking a number, and is built into the language; a module that declares its own Array names that one instead. An array type may be written wherever a type may: a variable, a field, a parameter, a return value, the type a reference refers to, or a generic's type argument (Option[Array[u8, 4]]). In a generic function or method the size may be a number parameter, Array[f32, N], and nothing computed from it (Value Parameters).

An array type may give several sizes, one for each dimension. Such an array is an array of arrays, the first size the outermost, so Array[f32, 2, 3] is the same type as Array[Array[f32, 3], 2]: two rows of three. Its first index picks the row and its second the element in that row, and the elements are stored row by row, the last index varying fastest:

let matrix Array[f32, 3, 3] = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]];
let val = matrix[1][1];  // 1.

That is how the array itself is laid out. A library type built over an array decides what its elements mean: geomath's Mat3 holds its nine values column by column in an Array[f32, 9].

Warning: Typically, when you want a collection type capability, you won't reach directly for an array, because of the fixed-size limitations and because you cannot implement methods directly on arrays. More likely, you will use one of the library-based collection types. If you look under the covers of these, you will see these are basically struct types that wrap around a specific array, or a pointer to elements they allocate (as well as possibly other fields), and lend their elements as an array reference. Being structs, they offer implemented methods to work with the collection and its elements.

Initialization#

To create or initialize an array, specify the values of all its elements separated by commas and enclosed within square brackets (as the above example demonstrates). When this array constructor is used to initialize a global variable, the element values must be constants. When used as part of a function's logic, element values may be specified using expressions:

let pair = [nbr * nbr, nbr + nbr];

An alternative way to create an array of bytes is to fill it from a string literal, where the array's type says how many bytes it is:

let hello Array[u8, 12] = "Hello world!";

A string is encoded as an sequence of utf-8 code points. The length is the number of bytes it occupies, and the array must be exactly that long: any other length is an error. The type of each element is u8. For C-compatibility, the zero terminator ('\0') is placed at the end of the string, but not counted in its length. A string literal that is not filling an array is not an array: it is a borrowed reference to its text, &str (A String as a Slice of Bytes describes how it converts to a slice of bytes, and owning references how it is copied into an owner).

Contents#

An array may also be constructed with its contents: new, its type, then <- and the values that fill it, in order. This is the send operator's list, which any collection takes, and the same forms may be used: values listed, n of x for n values, and fill x for as many as are left to fill. A value that is repeated is evaluated once for each element. This example creates a 100-byte string, where all bytes are spaces:

let spaces = new Array[u8, 100] <- fill ' ';
let row = new Array[f32, 4] <- 1.0, 3 of 0.0;

An array's size is fixed, so its contents give exactly that many values, and the count before of is a constant, from 0 through 4294967295. An array has no initializer, so the construction takes no parentheses, and a global's contents, like its array literal's elements, are constants.

The contents of an array written with several sizes are its scalars, the values of the element type written first, row by row, in the order the array stores them, so each entry counts scalars and n of x and fill run on across the rows. The contents give exactly as many as the product of the sizes, which may be at most 4294967295. Written nested, the array's elements are its rows, and its contents are those rows, each constructed in turn; inside the outer list, a row's own list is parenthesized. The two spellings are still one type, so either construction is assigned, passed and returned where the other is wanted:

let grid = new Array[i32, 2, 3] <- 1, 2, 3, 4, 5, 6;     // rows [1, 2, 3] and [4, 5, 6]
let cube = new Array[u8, 2, 3, 4] <- fill 0u8;           // all 24 bytes
let rows = new Array[Array[i32, 3], 2] <- fill new Array[i32, 3] <- (1, 2, 3);

An alias of an array type keeps the spelling it names. A generic's type parameter stands for a type, not a spelling, so through it an array's contents are its rows. An array written with several sizes whose element type is itself an array, Array[Array[u8, 4], 2, 3], takes no contents: whether they would be its bytes or its four-byte arrays is not decided, so it is written nested to be constructed with contents.

An allocation takes the same contents, and fills the array in the region's memory (owning references).

Getting the length#

Note: Not built, as the note above says: len on an array value is refused (Error 1025: Invalid operation on an array), so the example below does not compile. A slice's len is built (Slices).

To get a count of how many elements are in an array, use the len method:

let hello Array[u8, 5] = "Hello";
let length = hello.len;   // =5

Indexing#

A single element within an array may be accessed using an unsigned integer index value enclosed in square brackets. The specified index may be an expression. An index of 0 refers to the first element.

Indexing can be used to get or set the specified element:

var c_array Array[u8, 4] = "coal";
c_array[2] = c_array[1];    // the 3rd element is now the same as the second
// c_array now holds the array value "cool"

Array indexing is automatically bounds-checked by its known size. Any attempt to supply an invalid index results in a compile-time or run-time error. At run time it is a panic naming the index, the size and the line: index 4 is out of bounds for a count of 4.

Note: It is also possible to use the ampersand operator to borrow a reference to a specific indexed element in an array.

Indexing from the end#

Sometimes it is more convenient to index an element based on its position relative to the end of the array. This can be accomplished in a verbose way:

let nbrs = [2, 4, 6];
let four = nbrs[my.len - 2]; // =4 - the 2nd from the end

The less verbose equivalent uses the $- operator:

let four = nbrs[$-2];

$- is equivalent to my.len -

Iterating over an array#

Using indexing and the len method, one can now easily iterate over every element in an array:

let numbers = [3.5, 4.0, 5.5, 3.0];
var average = 0.0;
var i = 0;
while i < numbers.len {
  average += numbers[i++];
}
average /= numbers.len;      // == 4.0

A cleaner, more concise approach uses an each block instead:

let numbers = [3.5, 4.0, 5.5, 3.0];
var average = 0.0;
each nbr in numbers {      // nbr borrows each element
    average += nbr;
}
average /= 4.0;              // the number of elements: == 4.0

Multi-element copying#

Multiple elements of an array may be copied into another array using a range index.

var greetings Array[u8, 11] = "Hello Jeter";
greetings[7,4] = greetings[1,4]; // "Hello Jello"
greetings[6..10] = "Peter";      // "Hello Peter"
greetings[6..<11] = "Helen";      // "Hello Helen"
greetings[6..] = "Jayne";        // "Hello Jayne" (end is assumed if unspecified)

As the examples show, several range operators are supported. The first number is always the index of the first element in the segment. The meaning of the number after the range operator varies:

  • , - The second number specifies how many elements are in the array segment.
  • ..< - The second number specifies the index of the element after the last element in the array segment.
  • .. - The second number specifies the index of the last element in the array segment. If unspecified, it is assumed to be the last element of the array.

Both array segments must have the same element type and length. Array segments can only be used for copying elements from one array to another using assignment or variable initialization. An array segment cannot be passed as an argument in a function call or used in a comparison.

Note: One can also use the ampersand operator to borrow an array reference (slice) to a specific set of elements in an array.

Multi-element fills#

Using the same range operators, a single value can be repeatedly filled into a segment's elements:

var numbers = [0, -1, -2, -3];
numbers[1 ..< 3] = 2;
// numbers == [0, 2, 2, -3]

The type of the fill value must be the same as the type of the array's elements.

Copying an array#

Like numeric values, array values may be passed around between variables and functions:

let hello Array[u8, 5] = "Hello";
fn main() {
  var dessert = jello(hello);
}

fn jello(var saying Array[u8, 5]) Array[u8, 5] {
  saying = "jello";
  saying;
}

In this example, a copy of the hello variable's array is passed to the function jello. It replaces that copy's contents with the string "jello" and returns it to main. The mutable local variable dessert now holds a copy of the array string "jello".

When arrays are passed around, we always make copies of the array (just like we do with numbers). Any changes made to a copy won't change the original array. The exception is an array whose element type has move semantics: such an array moves rather than copies, whether its type is written out or inferred from an array literal.

This may not always be the behavior that you want, though. If instead you want to pass around a reference to an array, such that any changes do change the original array, obtain and pass an array reference.

Comparing arrays#

Two arrays may be compared, so long as they have the same element type.

To be equivalent the arrays must have the same length and every element must be the same. To make element comparison possible, the element's type must support the == method.

let somebody = "John";
if somebody == "Jane" {
  ....
}

Comparing for order (e.g., <) is also handled by comparing the two arrays element-by-element, stopping the first time two elements are not the same. The number of elements compared is the minimum of the lengths of the two arrays. If the element check shows them as equivalent, the lesser-sized array comes first in order.

The match statement or is operator is to support more array comparison, particularly partial pattern matching, which is not built yet.

Slices#

Note: Not implemented: a subslice's count form (,), an index relative to the end ($-), a slice made from a reference or a pointer, and copying and filling a range of elements. A borrowed array or string literal (&[1, 2, 3], &"Padma") is not yet a slice: it is a reference to the array, with no .len of its own. Making text from bytes, bytes as &str, does not yet check that the bytes are UTF-8, nor that nothing still writes them while the text is used.

An array reference, or slice, is a borrowed view of some number of elements of the same type, laid out one after another in memory. How many there are is known only at runtime, so the reference carries the count alongside the pointer to the first element. A slice does not own its elements: it is borrowed from something that does, and like any borrowed reference it may not outlive it.

Borrowing a Slice#

A slice may be borrowed from any array, from another slice, or from a collection that lends one:

let nbrSlice = &[1, 2, 3];
let textSlice = &"Padma";
let listSlice = list.view();    // from a List
let fillSlice = list.viewMut(); // from a List, its elements changeable: &mut Array[T]
let uniSlice = (&uni list).viewUni(); // from a List lent &uni: &uni Array[T]

These slices point to all elements of the source's array. (The first two borrow a literal: see below for how that differs from a slice made with &[].)

If a static array is passed as a call argument where a slice is expected, it is automatically converted into a slice:

print("abc");    // equivalent to print(&"abc")

Safety Note: Because a slice is a borrowed reference, its lifetime is limited, ensuring it can never outlive the source array it provides a view into. Depending on permissions, it may temporarily lock out access to the variable it borrows from, thereby ensuring the original array cannot be moved or resized while the slice is active.

Borrowing a Literal or a Constant#

A literal whose elements are all constants, an array literal or a string literal, is kept in read-only memory, and so is a named constant holding an array literal or the byte array a string literal fills. A borrow of one is a constant too: it lives as long as the program, so a function may return it, and it may not be mut.

Written &[1, 2, 3] or &"Padma", the borrow is a reference to the array (&Array[i32, 3]). It is taken wherever a slice is wanted, but it has no .len of its own. &[][1, 2, 3] is the slice, with one:

const primes Array[u32, 4] = [2u32, 3u32, 5u32, 7u32];

fn firstOf(p &Array[u32]) u32 { p[0]; }
fn smalls() &Array[i32] { &[1, 2, 3]; }       // the borrow outlives the call

let nums &Array[i32, 3] = &[1, 2, 3];        // a reference to the array: nums.len is refused
let sl &Array[i32] = &[][1, 2, 3];           // the slice: sl.len is 3
let two = firstOf(&primes);                 // a named constant, borrowed: 2

Borrowing a Computed Literal#

An array literal with a computed element, such as &[x, y], is a temporary. It lives to the end of its statement, or, where a variable's initializer borrows it, to the end of the block (see borrowing a temporary). A borrow of it cannot be returned from the function:

let s = &[][x, y];                  // lives to the end of the block
let t = total(&[x, y, 6]);          // lives to the end of the statement

Subslices#

It is also possible to create a new slice that points to some subset of the elements in some array or slice. This is accomplished by indexing using a range operator between two unsigned integers:

  • .. includes the last specified element.
  • ..< stops before the last specified element.
  • , specifies how many elements are in the slice.

The two range operators are the ones a number range of each and a range pattern use. For example:

let x = &"Abcd"[1..<3];   // a slice pointing to "bc"
let y = &"Abcd"[1..3];    // a slice pointing to "bcd"
let z = &"Abcd"[2, 1];  // a slice pointing to "c"

If either index integer is unspecified, 0 and end are assumed (so [..3] is the elements 0 through 3, [..<3] the first three, and [1..] everything from 1 on). A range is checked against the length of what it is taken from, as an index is: its start may not come after its end, nor its end after the last element. The subslice has the permission and lifetime a borrowed reference to one of those elements would have.

let r = &y[1..];        // a slice pointing to "cd" (end assumed)
let s = &y[..];         // a slice pointing to "bcd" (0 and end assumed)

An index integer may be specified relative to the last element:

let r = &y[$-2..];      // a slice pointing to "cd"

A slice may also be created from a regular reference or a pointer:

let slice1 = &ref[..];      // Slice count from reference is always 1
let slice2 = &ptr[0..5];

Operations#

Slices may be indexed, copied, compared, iterated over, and queried for their properties. Many of these operations work the same way they do with static arrays. A slice, and an array, calls the methods of core's Array[T]: iter(), indexed(), mutItems() (a &mut slice's, for changing elements in place), chunks(n) and mutChunks(n) (the elements in runs of n, each run a slice, a &mut one from mutChunks), which each walks.

Access an Element#

A single element within an array may be accessed using an unsigned integer value enclosed in square brackets. This supports both getting and setting the values at a specific index:

c_array[3] = c_array[2];    // the 4th element is now the same as the third

Indexing is automatically bounds-checked by its known size. An index out of bounds is a panic naming the index, the size and the line; so is a range, &s[a..<b], that starts after it ends or ends past the size.

Borrowed Reference to an Element#

One can create a borrowed reference to any element of a slice in the same way as for an static array:

let nbrRef = &nbrSlice[1];     // points to 2
let charRef = &textSlice[2];   // points to 'd'

Borrowing a reference to a single element is not a slice. It is just a regular borrowed reference to some value.

Copy or Fill Elements#

Multiple elements of an array may be copied into another array using a range in square brackets. Both content segments must have the same type and size.

arrref[1 by 2] = sliceref[0 by 2];
greetings[n by 5] = "Kevin";

Similarly, a single value can be repeatedly filled into a slice's elements:

arrref[1 ..< 3] = 2;

Iteration#

The each loop may iterate over a slice's elements:

var sum = 0;
each x in intslice {      // x borrows each element
    sum += x;
}

Each pass's variable is a borrow of the element, which cannot be changed through it. Walking mutItems() instead lends each element as a &mut to change it where it lies, and indexed() pairs each element with its position:

each x in mutslice.mutItems() {
    *x = *x + 1;
}
each i, x in intslice.indexed() {      // i is a usize
    sum += i32.from(i) * x;
}

Both are counted loops over the slice, as the plain each is (Changing items in place). A bare indexed() lends & on any slice, a &mut one too; mutslice.mutItems().indexed() lends each element as a &mut. chunks(n) and mutChunks(n) hand the elements out in runs of n, each run a slice (the last is what is left; n of 0 is a panic), and a parallel each gives each pass a run of its own:

each run in intslice.chunks(4) { ... }          // run is a slice of up to 4 elements
each y, row in mutslice.mutChunks(w).indexed() {
    row[0] = y;                                  // row is a &mut slice
}

Seen as Rows: asArrays#

asArrays[N]() sees a slice of T as a slice of Array[T, N] over the same memory: nothing is copied, and element k of row i is element i * N + k of the slice. N is a number parameter written in brackets, since no argument says it. mutArrays[N]() is the same on a &mut slice and gives a &mut one, so writing a row's element changes the original (it has a name of its own, as mutChunks has beside chunks). Both are methods of an array, a slice and a list. A count that is not a multiple of N, and an N of 0, are a panic at the line of the call.

let bytes = &[][1u8, 2u8, 3u8, 4u8, 5u8, 6u8];
let rows = bytes.asArrays[3]();      // &Array[Array[u8, 3]]: [1 2 3] [4 5 6]
let c = rows[1][2];                  // 6
let rows3 = pixels.mutArrays[3]();
rows3[0][1] = 9u8;                 // changes pixels[1]

The rows are a slice like any other, so the runs cursors cut them: a picture of bytes, three to a pixel, is written a row to a pass, a pixel's channels by index, and leaves the bytes the same loop leaves when it computes x * 3 + k itself:

parallel each y, line in pixels.mutArrays[3]().mutChunks(width).indexed() {
    each x in 0usize ..< width {
        line[x][0] = red(x, y);
        line[x][1] = green(x, y);
        line[x][2] = blue(x, y);
    }
}

Comparison#

Two slices may be compared for identity with === (or !==), so long as they have the same type. This comparison checks for equality of both the starting pointer and the element count, so it asks whether the two view the same elements, not whether their elements are equal.

if slice1 === slice2 {
   ....
}

As with other references, == (or !=) on two slices compares what they refer to, element by element. Two slices are equal when they have the same number of elements and each element is == to the element at the same index in the other. Each pair is compared with the element type's own ==: a number's or a bool's, or one a struct declares; a slice of slices compares each inner slice the same way. Floating point elements compare as floating point numbers do, so a NaN is not equal to any element, itself included, and -0.0 is equal to 0.0. A slice whose element type has no == cannot be compared this way.

let name &Array[u8] = "Padma";
if name == "Padma" {   // true: the same five bytes
   ....
}

What a slice is compared with may be anything that would be accepted where that slice is wanted: another slice, an array or a reference to one, or a string literal. An array or a reference to one may be on the left too.

Comparing for order (e.g., <) is not possible directly. One can of course compare the component parts of a slice separately.

Property Queries#

A slice is a "fat" reference, as it holds two pieces of information:

  • a pointer to the first element of the array
  • an unsigned integer specifying how many elements are in the array

The count is len: let len = slice.len;.

Note: Not built yet: the pointer, slice.ptr, which is refused (Error 1025: Invalid operation on an array ref).