Vectors and collections
Metaxu has two vector types with two different characters. Vec is the growable heap collection: push, pop, index, one identity shared by everyone holding it. vector[T, N] is a fixed-size value: its length is part of its type, it copies on assignment, and arithmetic on it is elementwise. This chapter covers both, plus strings.
Vec
Vec.new() makes an empty vector; a literal [a, b, c] makes a full one. push appends, pop removes and returns the last element, len answers the count, and v[i] indexes from zero, for reading or writing:
metaxufn main() -> int {
let @mut v = Vec.new();
v.push(10);
v.push(20);
v.push(30);
print(v.len());
print(v[1]);
v[1] = 25;
print(v[1]);
print(v.pop());
print(v.len());
let primes = [2, 3, 5, 7];
print(primes[3]);
0
}
len(v) and v.len() are the same call; std/ uses the function form, most examples the method form.
Vec has identity
Assigning a Vec to another name or passing it to a function hands over the same vector, not a copy. That's the point: a helper that pushes into its argument is how you build things.
metaxufn extend_with_squares(target: Vec, upto: int) {
let @mut i = 0;
while i < upto {
target.push(i * i);
i = i + 1
}
}
fn main() -> int {
let @mut v = Vec.new();
let same = v;
extend_with_squares(v, 4);
print(same.len());
print(same[3]);
0
}
Both same and the callee saw v itself change. Contrast with structs, which chapter 2 showed pass as copies unless the parameter says @mut.
Out of bounds is a failure you can catch
Indexing past the end doesn't read garbage and doesn't kill the process behind your back. It's a runtime failure, and try/catch (chapter 9 has the full story) turns it into a value:
metaxufn main() -> int {
let v = [1, 2];
let got = try {
v[10]
} catch e {
print("caught: " + e);
-1
};
print(got);
let @mut empty = Vec.new();
let popped = try { empty.pop() } catch e { 0 };
print(popped);
0
}
outputcaught: index out of bounds: 10 (length 2)
-1
0
Iterating
for walks a Vec, or an integer range a..b (end exclusive). When you need the index too, use the while i < len(v) loop, the idiom all of std/vec.mx is written with; extend_with_squares above is one:
metaxufn main() -> int {
let names = ["ada", "grace", "edsger"];
for name in names {
print(name);
}
for k in 0..3 {
print(k * 10);
}
0
}
outputada
grace
edsger
0
10
20
There is no slice syntax; std.sort builds its halves with an explicit copy loop for the same reason you would.
std.vec and std.sort
The combinators live in the library, not the language. import std.vec gives map, filter, sum, reverse and friends, eager and Vec-in, fresh-Vec-out; std.sort adds a stable merge sort driven by a strict less-than predicate:
metaxuimport std.vec
import std.sort
fn main() -> int {
let v = std.vec.range_vec(1, 6);
print(std.vec.sum(v));
let evens = std.vec.filter(v, fn(x) -> x % 2 == 0);
print(evens.len());
let sorted = std.sort.sort_desc([3, 1, 4, 1, 5]);
print(sorted[0]);
print(sorted[4]);
0
}
Fixed vectors: vector[T, N]
A vector[T, N] holds exactly N elements of T. Construct it with values, with nothing (zero-initialized), or with a comprehension. Arithmetic is elementwise, and the operands' sizes must agree:
metaxufn main() -> int {
let a = vector[int, 4](1, 2, 3, 4);
let b = vector[int, 4](10, 20, 30, 40);
let c = a + b;
print(c.to_string());
print((a * a).to_string());
let z = vector[int, 3]();
print(z[0]);
let sq = vector[float, 4](x * 0.5 for x in 0..4);
print(sq.to_string());
0
}
outputvector[11, 22, 33, 44]
vector[1, 4, 9, 16]
0
vector[0.0, 0.5, 1.0, 1.5]
Because N is in the type, the native backend compiles these to real SIMD when it can prove the size, which is why they exist as a separate type instead of a small Vec.
Fixed vectors are values
Unlike Vec, a vector[T, N] copies on assignment. Writing through one name never surprises another:
metaxufn main() -> int {
let @mut a = vector[int, 3](1, 2, 3);
let @mut b = a;
b[0] = 99;
print(a[0]);
print(b[0]);
0
}
Strings
Strings concatenate with +, compare with == and order with <, and index to one-character strings; there's no separate char type. len counts characters:
metaxufn main() -> int {
let s = "metaxu";
print(len(s));
print(s[2]);
print(s + "!");
if "abc" < "abd" {
print("abc sorts first");
}
let @mut spelled = "";
let @mut i = len(s);
while i > 0 {
spelled = spelled + s[i - 1];
i = i - 1
}
print(spelled);
0
}
output6
t
metaxu!
abc sorts first
uxatem
Five builtin methods do the text work that would otherwise be a loop over s[i], and they run in linear time on both engines: split gives a Vec of the pieces between a separator, find the index of a substring or -1, replace a copy with every occurrence swapped, trim a copy without leading or trailing space, tab, newline or carriage return, and join, on a Vec of strings, one string with a separator between the parts. An empty separator for split and an empty pattern for replace are errors, so try catches them:
metaxufn main() -> int {
let line = " name = metaxu ";
let fields = line.trim().split(" = ");
print(len(fields));
print(fields[1]);
print("mississippi".find("ss"));
print("mississippi".find("xyz"));
print("a-b-c".replace("-", ", "));
print(fields.join("="));
let caught = try { let p = "x".split(""); "no error" } catch e { e };
print(caught);
0
}
output2
metaxu
2
-1
a, b, c
name=metaxu
split: empty separator
They are also callable as plain functions, split(line, ","), like len.
Bytes are a Vec of ints from 0 to 255. to_bytes gives a string's UTF-8 bytes and from_bytes turns such a Vec back into a string, refusing a value outside the byte range, a zero byte, or invalid UTF-8 with a catchable error. std.hex prints bytes as hex and std.sha256 hashes them:
metaxufrom std.hex import to_hex;
from std.sha256 import sha256_string;
fn main() -> int {
let b = "hé".to_bytes();
print(len(b));
print(to_hex(b));
print(b.from_bytes());
print(sha256_string("abc"));
let @mut broken = Vec.new();
broken.push(195);
print(try { broken.from_bytes() } catch e { e });
0
}
output3
68c3a9
hé
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
from_bytes: invalid UTF-8 at byte 0
std.string packages the character-level loops you'd otherwise rewrite: starts_with, count_char, repeat, and more:
metaxuimport std.string
fn main() -> int {
print(std.string.repeat("ab", 3));
if std.string.starts_with("metaxu", "meta") {
print("has the prefix");
}
print(std.string.join(["a", "b", "c"], ", "));
print(std.string.count_char("banana", "a"));
0
}
outputababab
has the prefix
a, b, c
3
That's the eager story. std.stream and std.iter do the lazy version, pipelines built from the Emit effect, and they belong to chapter 8, after effects themselves. Next, though: the types you never had to write in any of these examples, and where they came from.