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;
let mut 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
}
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() {
let mut 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() {
let mut 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 {
let mut 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 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.