Expressions
9.1 General
Section titled “9.1 General”Koine has one small, pure expression language. It is the same language everywhere a value or condition is expected: in derived fields (§5), invariants (§10), command and factory bodies, specs and service operations (§13), and read-model projections (§15).
“Pure” is the whole point: no statements, no assignments, no loops, no I/O, no null literal. An expression
is a value computed from a field, a parameter, a literal, and a fixed set of operators and built-in operations.
Everything below translates to idiomatic C# — a derived field becomes a get-only computed property, an
invariant becomes a constructor guard.
9.1.1 Where expressions are allowed
Section titled “9.1.1 Where expressions are allowed”| Position | Form | Example |
|---|---|---|
| Derived field | name: Type = expr | total: Money = lines.sum(l => l.payable) |
| Invariant | invariant expr "message" | invariant amount >= 0 "a price cannot be negative" |
| Guarded invariant | invariant body when cond | invariant status == Draft when lines.isEmpty |
| Command precondition | requires expr "message" | requires !lines.isEmpty "cannot submit an empty order" |
| Spec body | spec S on T = expr | spec IsVip on Customer = tier == Gold |
| Operation body | operation o(...): T = expr | operation discountRate(tier: LoyaltyTier): Decimal = ... |
| Read-model field | name: Type = expr | lineCount: Int = lines.count |
| Factory init / command transition | field -> expr | total -> lines.sum(l => l.price) |
9.2 Syntax
Section titled “9.2 Syntax”expression : let_expr ;
let_expr : 'let' let_binding ( ',' let_binding )* 'in' let_expr // let x = e, y = e in body | guard_expr ;let_binding : Identifier '=' expression ;
guard_expr : cond_expr ( 'when' cond_expr )? ; // expr when cond
cond_expr : 'if' cond_expr 'then' cond_expr 'else' cond_expr // if c then a else b | coalesce_expr ;
coalesce_expr : or_expr ( '??' or_expr )* ;or_expr : and_expr ( '||' and_expr )* ;and_expr : equality_expr ( '&&' equality_expr )* ;equality_expr : relational_expr ( ( '==' | '!=' ) relational_expr )* ;relational_expr : match_expr ( ( '<' | '<=' | '>' | '>=' ) match_expr )* ;match_expr : additive_expr ( 'matches' Regex )? ; // raw matches /.../additive_expr : multiplicative_expr ( ( '+' | '-' ) multiplicative_expr )* ;multiplicative_expr: unary_expr ( ( '*' | '/' ) unary_expr )* ;unary_expr : ( '!' | '-' ) unary_expr | postfix_expr ;postfix_expr : primary ( '.' Identifier ( '(' arg_list? ')' )? )* ;
arg_list : argument ( ',' argument )* ;argument : lambda | expression ;lambda : Identifier '=>' expression ; // l => l.quantity > 0
primary : literal | Identifier | '(' expression ')' ;literal : DecimalLiteral | IntLiteral | StringLiteral | BoolLiteral ;The let … in form binds intermediate names within an expression and nests anywhere a value is
expected:
total: Money = let net = lines.sum(l => l.payable) in net * taxRateOperators bind from lowest precedence (top) to highest (bottom):
| Precedence | Form | Operators | Associativity |
|---|---|---|---|
| 1 (lowest) | binding | let … in … | — |
| 2 | guard | expr when cond | non-associative |
| 3 | conditional | if … then … else … | right (nests in else) |
| 4 | coalesce | ?? | left |
| 5 | logical or | || | left |
| 6 | logical and | && | left |
| 7 | equality | == != | left |
| 8 | relational | < <= > >= | left |
| 9 | match | matches /…/ | non-associative (infix) |
| 10 | additive | + - | left |
| 11 | multiplicative | * / | left |
| 12 | unary | prefix ! - | right |
| 13 | postfix | .member, .op(args) | left |
| 14 (highest) | primary | literal, name, ( … ) | — |
9.2.1 Literals and identifiers
Section titled “9.2.1 Literals and identifiers”The atoms of every expression:
- Numbers —
10,0.9,0.0(a decimal literal carries no suffix in.koi; the emitter addsmforDecimal). - Strings —
"X",", "(double-quoted). - Booleans —
true,false. - Identifiers — a field name, a factory/command parameter, or a bare enum member (resolved against the field’s enum type, so two enums may share a member name).
isAvailable: Bool = availability == InStockHere availability is a field and InStock is a bare member of its enum — both are identifiers.
9.3 Logical operators
Section titled “9.3 Logical operators”Boolean logic uses && (and), || (or), and prefix ! (not).
spec IsLargeOrder on Order = lines.count > 10 || total.amount > 1000requires !lines.isEmpty "cannot submit an empty order"9.4 Arithmetic
Section titled “9.4 Arithmetic”Arithmetic + - * / works as you expect. Arithmetic over a value object uses that object’s generated
operators (so unitPrice * quantity multiplies Money by a scalar).
value OrderLine { product: ProductId quantity: Int unitPrice: Money lineTotal: Money = unitPrice * quantity invariant quantity >= 1 "an order line needs at least one unit"}9.5 Comparison
Section titled “9.5 Comparison”Comparison operators == != < <= > >= are type-checked. Relational operators (< <= > >=) require
orderable operands — exactly Int, Decimal, and Instant. String is not orderable; compare
strings with ==/!= only.
Instant fields (emitted as DateTimeOffset) compare with the full relational set < <= > >= == !=.
Comparing an Instant against a non-Instant is a type error.
value DateRange { startsAt: Instant endsAt: Instant invariant startsAt <= endsAt "start must precede end"}The built-in now is recognized in command bodies (e.g. submittedAt -> now) but is rejected as a stored
default (field: Instant = now) so generated models stay deterministic.
9.6 Conditionals
Section titled “9.6 Conditionals”if cond then a else b is an expression (a ternary), not a statement — it always yields a value, so both
branches are required and must have compatible types.
payable: Money = if quantity >= 10 then lineTotal * 0.9 else lineTotalIt emits a parenthesized C# ternary:
public Money Payable => ((Quantity >= 10) ? (LineTotal * 0.9m) : LineTotal);Conditionals nest in the else branch for multi-way logic:
operation discountRate(tier: LoyaltyTier): Decimal = if tier == Gold then 0.10 else if tier == Silver then 0.05 else 0.09.7 Guards
Section titled “9.7 Guards”A boolean expression may be qualified with a when guard, written body when condition. The guard is
how a conditional invariant reads: the body is only required to hold when the condition is true.
invariant status == Draft when lines.isEmpty "an empty order must stay in Draft"when sits just above the conditional expression in precedence (§9.2) and is
non-associative — a single optional guard per expression.
9.8 Pattern matching
Section titled “9.8 Pattern matching”For shape constraints beyond equality, use the regex form field matches /pattern/ in an invariant.
matches is a non-associative postfix operator at precedence 9 (§9.2), applied to any
String expression.
value Iban { code: String invariant code matches /^[A-Z]{2}[0-9]{2}[A-Z0-9]+$/ "must look like an IBAN"}See Invariants (§10) for the full matches and when guard coverage.
9.9 Collection operations
Section titled “9.9 Collection operations”Collection operations apply to a List<T> field. The element type T is in scope inside a lambda written
param => expr, with the element’s members resolvable (l => l.quantity).
| Koine | Meaning | Emitted C# (sketch) |
|---|---|---|
xs.count | element count | Xs.Count |
xs.isEmpty | is empty | Xs.Count == 0 |
xs.isNotEmpty | is non-empty | Xs.Count != 0 |
xs.all(l => p) | every element satisfies p | Xs.All(l => p) |
xs.any(l => p) | some element satisfies p | Xs.Any(l => p) |
xs.sum(l => e) | fold e over elements | numeric: Xs.Sum(l => e); value object: Xs.Select(l => e).Aggregate((a, b) => a + b) |
xs.distinctBy(l => k) | no two elements share key k | Xs.Select(l => k).Distinct().Count() == Xs.Count |
entity Order identified by OrderId { lines: List<OrderLine> total: Money = lines.sum(l => l.payable) lineCount: Int = lines.count invariant lines.all(l => l.quantity >= 1) "every line needs a positive quantity" invariant lines.distinctBy(l => l.product) "no duplicate products in an order"}lines.sum(l => l.payable) over a value-object selector folds with the element’s + operator rather than
numeric .Sum(...):
public Money Total => Lines.Select(l => l.Payable).Aggregate((a, b) => a + b);public int LineCount => Lines.Count;9.9.1 String operations
Section titled “9.9.1 String operations”String operations are written as member access on a String receiver. They chain left to right.
| Koine | Meaning | Emitted C# |
|---|---|---|
s.length | character count | s.Length |
s.trim | strip surrounding whitespace | s.Trim() |
s.upper | upper-case | s.ToUpperInvariant() |
s.lower | lower-case | s.ToLowerInvariant() |
value Sku { code: String normalized: String = code.trim.upper invariant code.trim.length > 0 "a SKU cannot be blank"}code.trim.upper chains, emitting Code.Trim().ToUpperInvariant(); the invariant
code.trim.length > 0 becomes a constructor guard if (!(code.Trim().Length > 0)) throw ….
9.10 Optionality
Section titled “9.10 Optionality”Mark a field optional with a trailing ? (String?, Instant?). Optional fields default to absent and are
excluded from non-null construction guards. Three expression forms work with optionals:
| Koine | Meaning | Emitted C# |
|---|---|---|
a ?? b | coalesce — a if present, else b | (a ?? b) |
field.isPresent | true when set | field is not null |
field.isNone | true when absent | field is null |
entity Customer identified by CustomerId { name: String nickname: String? phone: String? displayName: String = nickname ?? name hasPhone: Bool = phone.isPresent}public string DisplayName => (Nickname ?? Name);public bool HasPhone => Phone is not null;Coalescing operands of different numeric types
Section titled “Coalescing operands of different numeric types”?? may join an Int-typed operand with a Decimal-typed one (total: Decimal? = hint ?? fallback
where hint: Int?) — the semantic model widens the pair the same way it widens the two branches of an
if … then … else. Because most targets require both sides of their coalesce construct to agree in
type, each emitter reconciles the narrower operand against the wider one when it renders the
coalesce, rather than emitting the two disagreeing types verbatim:
| Target | best: Decimal? = hint ?? fallback where hint: Int?, fallback: Decimal? |
|---|---|
| C# | (Hint ?? Fallback) — an implicit int→decimal conversion covers it |
| TypeScript | (((__v: number | undefined) => (__v === undefined ? undefined : Decimal.fromInt(__v)))(this.hint) ?? this.fallback) |
| Python | (Decimal(__koine_v) if (__koine_v := self.hint) is not None else self.fallback) |
| PHP | ((fn($__v) => $__v === null ? null : new \Koine\Runtime\Decimal($__v))($this->hint) ?? $this->fallback) |
| Kotlin | this.hint?.let { java.math.BigDecimal.valueOf(it) } ?: this.fallback |
| Java | this.hint.map(java.math.BigDecimal::valueOf).or(() -> this.fallback) |
Each operand is reconciled against the other operand’s type independently, so the widening lands on whichever side is narrower — swap the two above and the widen moves to the right-hand side. A coalesce whose operands already agree emits the plain construct with no wrapping.
9.11 Translation to C#
Section titled “9.11 Translation to C#”Every expression form has a direct C# rendering shown inline in the sections above. In summary:
- Derived fields (
name: Type = expr) → get-only computed properties (public T Name => …;). - Invariants (
invariant expr "msg") → constructor guards (if (!(…)) throw new ArgumentException("msg")). - Guarded invariants (
invariant body when cond) → guards wrapped with the condition (if (cond && !(body)) throw …). let … inbindings → local variables or inlined expressions in the emitted property body.if … then … else …→ parenthesized C# ternary ((cond ? a : b)).??→ C# null-coalescing operator ((a ?? b))..isPresent/.isNone→field is not null/field is null.- Collection ops → LINQ (
All,Any,Sum,Select,Aggregate,Count,Distinct). - String ops →
Trim(),ToUpperInvariant(),ToLowerInvariant(),Length. - Atomic token note —
->and<->are single, indivisible tokens; see §3.7 for the atomic-token rule.
See also
Section titled “See also”- Value objects (§5) — derived fields use expressions.
- Invariants (§10) — boolean expressions plus
matchesandwhenguards. - Commands, events & state machines (§11) and Factories (§12) — bodies use
requires,->, andemit. - Specifications, services & policies (§13) — named expression bodies.
- Application layer & CQRS (§15) — read-model projections use expressions.