Tutorial: Numbers, Characters, and Collections
This tutorial covers several features added or expanded in Metel v0.8.0: 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[].
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() {
Perhaps::Some { value } => println("last stage: ${value}"),
Perhaps::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) {
Perhaps::Some { value } => println(value), // 2
Perhaps::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
}
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
i64andf64the default numeric forms in source examples. - Numeric conversions are explicit with
as. Charis a first-class scalar type, separate fromString.[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.
Next: Structs and Methods — define your own types and attach behaviour to them.