9. Reusable, and split into files
Two last steps: make a helper generic so it survives the event shape changing
again, and break metrics into modules.
A generic reducer over a row bound
You've already used row bounds on single events (page 5). The same bound makes a reducer that doesn't care what else an event carries:
fun total_weight<record T: { user: i64, weight: i64, .. }>(events: T[]) -> i64 {
var sum := 0;
for (e in events) { sum := sum + e.weight; }
return sum;
}
fun main() {
let a := [
{ user = 7, weight = 42 },
{ user = 3, weight = 1 },
];
let b := [
{ user = 7, weight = 8, kind = "buy", line = 4 }, // wider — extra fields
{ user = 3, weight = 1, kind = "click", line = 5 },
];
println(total_weight(a)); // 43
println(total_weight(b)); // 9 — same function, richer records
}
43
9
total_weight never mentions kind or line; { user, weight, .. } already
covered "and whatever else". Add a bucket field next month and it still compiles.
Modules
Each .mtl file is a module. import brings names in; public is what a module
exposes. :: in a path maps to / on disk.
metrics/parse.mtl:
public struct ParseError { public line: i64, public reason: String }
public fun parse_int(s: String, line: i64) -> Result<i64, ParseError> {
// ...as written on page 3
return Ok { value = 0 };
}
metrics/report.mtl:
public fun total_weight<record T: { user: i64, weight: i64, .. }>(events: T[]) -> i64 {
var sum := 0;
for (e in events) { sum := sum + e.weight; }
return sum;
}
metrics/main.mtl:
import parse::{parse_int, ParseError};
import report::total_weight;
fun main() {
let events := [{ user = 7, weight = 42 }, { user = 3, weight = 1 }];
println("total ${total_weight(events)}");
}
Run the entrypoint:
cargo run -- metrics/main.mtl
Struct fields are private by default even when the struct is public — hence
public line / public reason on ParseError so main can read error.line.
Don't name a module std
The top-level name std is the standard library. A file std.mtl at your project
root is a compile error; any other name is fine.
What you built
metrics parses raw event lines, validates them into one enriched shape, routes and
tallies them through a capturing closure, and renders the result through a dyn
aspect — and at every stage a function asks for exactly the fields it reads. That's
the throughline: the data's shape is precise and it's allowed to change, and the
type system follows along.
From here: the reference pages for the parts of the language this tour skimmed, and the language specification for the normative detail.