Skip to main content
Next (unreleased) · docs c218d875 · core f63c1e69 · built 2026-10-26 22:39 UTC

5. Ask for less than the whole record

The reporting functions coming up only read one or two fields. Metel lets a generic function say exactly that.

Row bounds​

<record T: { user: i64, .. }> is a bound: T is any record that at least has a user: i64 field. The .. quantifies over whatever else it carries.

fun user_of<record T: { user: i64, .. }>(e: T) -> i64 { e.user }
fun is_heavy<record T: { weight: i64, .. }>(e: T) -> boolean { e.weight >= 10 }

fun main() {
let ev := { user = 7, kind = "buy", weight = 42, line = 2 };
let other := { user = 3, weight = 1 }; // different shape, same `user`/`weight` bounds

println(user_of(ev)); // 7
println(is_heavy(ev)); // true
println(user_of(other)); // 3
println(is_heavy(other)); // false
}
7
true
3
false

user_of accepts both records — it never mentions the fields they don't share. When you enrich the event again later (adding a bucket field, say), user_of and is_heavy keep working with no change: the bound already allowed "and more".

Records only​

A row bound is satisfied by a record, not by a nominal struct:

struct Point { x: i64, y: i64 }

fun x_of<record T: { x: i64, .. }>(v: T) -> i64 { v.x }

fun main() {
let p := Point { x = 1, y = 2 };
let n := x_of(p); // rejected: Point is a struct, not a record
}

A struct passed to a row-bounded parameter is a type error. This is the mirror of the rule that a struct's row is invisible to structural matching — its identity is its name.

If the field names are the contract, declare a named record instead. A record has a name, methods and a declaration like a struct, but its row is visible to bounds, so it satisfies them. Every field of a record is public, so fields are written bare:

record Point { x: i64, y: i64 }

fun x_of<record T: { x: i64, .. }>(v: T) -> i64 { v.x }

fun main() {
let p := Point { x = 1, y = 2 };
println(x_of(p)); // a named record satisfies the bound
}
1

Turning a struct into a record later is safe for callers; turning a record back into a struct is not, because anyone with a bound naming its fields stops compiling.

By the end you'll have​

user_of and is_heavy, each reading a slice of any event that has the field.

Rows can do more than bound a parameter — name "the rest" of a record, split a row, or attach methods that exist only for some rows. That is in Reference: Records and Rows.

Next: Move a slice, don't copy it — hand disjoint parts of one event to two consumers.