Expressions in Tenet
Tenet expressions are mostly familiar but with a few distinctive features, particularly around deconstruction and escaping.
Basic Operators
Tenet supports the usual arithmetic, comparison, and logical operators:
- Arithmetic:
+,-,*,/(floor division),%(modulo) - Comparison:
==,eq,!=,ne,lt,le,gt,ge - Logical:
not,and,or - Concatenation and unions:
++(for lists, strings, sets and maps)
Compound operaors
- Arithmetic:
+=,-=,*=,/=,%= - Logical:
and=,or= - Concatenation:
++=
Deconstruction Operators
Tenet provides two main parametric deconstruction operators that extract parts of composite values.
Record Field Access .
The dot operator . extracts a field from a record. The field name is a compile-time parameter.
let p = (x: 3, y: 4)
p.x == 3
p.y == 4
Union Variant Unpacking !
The bang operator ! unpacks a tagged value from a union. The tag is a compile-time parameter.
If the value has the expected tag, the variant is returned. If the tag doesn’t match, the entire value escapes.
let result = some-func() // returns ok~42 or #miss
let value = result ! ok // succeeds → 42; fails → escapes
This is directly analogous to record access:
.field-name→ parametric on field name (for Records)!tag→ parametric on tag (for Unions)
Expression-level Pattern Matching ? and ?!
The ? operator is the expression-level counterpart to the when statement. It performs pattern matching and is always exhaustive — all cases must be covered, and all branches must produce a value with a common supertype.
let desc = some-union ? {
ok ~ v -> "success: " ++ v
#miss -> "not found"
#error ~ e -> "failed with " ++ e
}
The variant ?! (question-bang) is similar but does not have to be exhaustive if the unmatched cases are allowed to escape according to the surrounding context.
let value = risky-result ?! {
ok ~ v -> v
#miss -> 0
// other tags are allowed to escape
}
? and ?! are useful when you want pattern matching inside a larger expression without introducing a full when block.
Function calls
Functions are invoked with named arguments, but the early arguments can be passed positionally in the same order as in the function definition:
// All identical
process(data: input, mode: #strict);
process(input, mode: #strict);
process(input, #strict);
Block arguments are constructed at the call site:
let processed = with-handler(input) { line =>
reply " | " ++ line ++ " | "
}
Blocks can also be included inside the parens like a regular argument:
let processed = with-handler(input, { line =>
reply " | " ++ line ++ " | "
})
// Full arguments
let processed = with-handler(input: input) handle: { line =>
" | " ++ line ++ " | "
}
// Passing blocks inside the parens
let processed = with-handler(input, handle: { line =>
" | " ++ line ++ " | "
})
Future / Deferred
- Full first-class functions (beyond block types)
- More powerful lensing and in-place mutation
- Generic arguments and more details