Skip to main content
v0.10.0

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
}
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 = c.to_u32();
println(code); // 65

match Char::from_u32(66) {
Perhaps::Some { value } => println(value), // B
Perhaps::None => println("invalid code point"),
}
}

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
}

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
}
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 and can match by shape.
  • 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.