Skip to main content
v0.14.0

Reference: Numbers, Characters, and Collections

Reference material — dip in as needed. Exact-width numeric types, Char, fixed-size arrays, List<T>, and explicit generic calls with turbofish.

Exact-width numeric types​

Metel uses explicit numeric types. The most common defaults are i64 for integers and f64 for floating-point values, and exact-width types are available when size matters:

  • signed integers: i8, i16, i32, i64
  • unsigned integers: u8, u16, u32, u64
  • floats: f32, f64
fun main() {
let red: u8 := 255;
let samples: i32 := 2048;
let ratio: f32 := 0.5;

println("red = ${red}");
println("count = ${samples}");
println("ratio = ${ratio}");
}

Unsuffixed numeric literals adopt the type the surrounding context requires:

fun main() {
let port: u16 := 8080;
let gain: f32 := 1.25;
var retries: i32 := 0;

retries := 3; // 3 is inferred as i32 from the binding
}

When there is no stronger context, integer literals default to i64 and float literals default to f64.

Explicit casts​

Metel does not perform implicit numeric conversions. Use as when you want to convert:

fun main() {
let width: u16 := 640;
let area_hint: i64 := width as i64 * 2;
println(area_hint); // 1280
}

This is especially relevant for indices. Array indexing requires u64:

fun main() {
let items := ["zero", "one", "two"];
let idx: u64 := 1;
println(items[idx]); // one
}

An unannotated array literal like ["zero", "one", "two"] has a fixed-size array type, [String; 3] here — see Fixed-size arrays below for what that means and how it differs from T[].

tip

Keep Index Types Honest

If a value represents a position into an array, declare it as u64 early instead of sprinkling as u64 casts later.

Characters with Char​

Char represents a single Unicode scalar value. Character literals use single quotes:

fun main() {
let initial: Char := 'M';
let newline: Char := '\n';
let smile: Char := '\u{1F600}';

println(initial);
println(smile);
}

You can convert between Char and its scalar value explicitly:

fun main() {
let c: Char := 'A';
let code: u32 := u32::from(c);
println(code); // 65

let back: Char := Char::from(66);
println(back); // B
}

Char is not a one-character string. Treat it as its own type.

Fixed-size arrays​

[T; N] is an array with a length known at compile time:

fun main() {
let rgb: [u8; 3] := [255, 160, 64];
let zeros: [i32; 4] := [0; 4];

println(rgb.len()); // 3
println(zeros[2u64]); // 0
}

Fixed-size arrays are useful when the size is part of the meaning of the value: RGB colors, 3D vectors, weekday tables, and similar fixed layouts.

They also participate in pattern matching:

fun describe_triplet(xs: [i64; 3]) -> String {
match (xs) {
[0, 0, 0] => "all zero",
[a, b, c] => "sum = ${(a + b + c).to_string()}",
}
}

When a function expects a normal array (T[]), a fixed-size array coerces automatically:

fun first(xs: [i64]) -> i64 {
xs[0u64]
}

fun main() {
let values: [i64; 3] := [10, 20, 30];
println(first(values)); // 10
}

T[] and [T; N] are not interchangeable in both directions. T[] is a borrowed, read-only view — a pointer and a length, never an owner of its elements. It only ever comes from borrowing a [T; N], a List<T>, or another slice, the way first above receives one; it is never what an array literal itself produces. [T; N], by contrast, owns its elements and can be mutated directly:

fun main() {
var values: [i64; 3] := [10, 20, 30];
values[0u64] := 99;
println(values[0u64]); // 99
}

Mutating through a T[] (xs[0u64] = 99 inside first above, for instance) is rejected at compile time — mutation is only available through [T; N] or List<T> directly. If you need a function to mutate the caller's array in place, take a mutable reference to it instead — xs: &var [i64; 3] — rather than xs: i64[].

Growable collections with List<T>​

Use T[] and [T; N] for array-style data. Use List<T> when the collection needs to grow and shrink at runtime.

fun main() {
var queue: List<String> := List::new();
queue.push("parse");
queue.push("typecheck");
queue.push("evaluate");

println(queue.len()); // 3

match (queue.pop()) {
Some { value } => println("last stage: ${value}"),
None => println("queue was empty"),
}
}

List::from(arr) builds a list from an existing array:

fun main() {
var nums := List::from([1, 2, 3]);
nums.push(4);

match (nums.get(1)) {
Some { value } => println(value), // 2
None => println("missing"),
}
}

If another function expects an immutable array view, call .as_slice():

fun total(xs: [i64]) -> i64 {
var sum := 0;
for (let x in xs) {
sum += x;
}
return sum;
}

fun main() {
let nums := List::from([5, 10, 15]);
println(total(nums.as_slice())); // 30
}
note

Choose The Collection By Shape

Use [T; N] when the size is fixed by the problem. Use T[] when the data is an array but does not need mutation. Use List<T> when push/pop-style mutation is part of the design.

Explicit generic calls with turbofish​

Most generic calls do not need any help from you, but sometimes you want to spell out the type arguments explicitly. Use turbofish syntax:

fun identity<T>(value: T) -> T {
return value;
}

fun main() {
let x := identity::<i64>(42);
let y := identity::<String>("hello");

println(x);
println(y);
}

This is most useful when inference would otherwise be ambiguous, or when you want the call site to show the intended concrete types clearly.

What you learned​

  • Metel v0.8.0 adds exact-width numeric types and makes i64 and f64 the default numeric forms in source examples.
  • Numeric conversions are explicit with as.
  • Char is a first-class scalar type, separate from String.
  • [T; N] represents fixed-size arrays, owns its elements, can match by shape, and can be mutated directly. An unannotated array literal produces [T; N].
  • T[] is a borrowed, read-only view — never what a literal produces, and never mutable through it.
  • List<T> is the growable collection type for push/pop-style workflows.
  • ::<...> lets you provide explicit generic arguments when inference needs help.