Skip to content

Expressions

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.

PositionFormExample
Derived fieldname: Type = exprtotal: Money = lines.sum(l => l.payable)
Invariantinvariant expr "message"invariant amount >= 0 "a price cannot be negative"
Guarded invariantinvariant body when condinvariant status == Draft when lines.isEmpty
Command preconditionrequires expr "message"requires !lines.isEmpty "cannot submit an empty order"
Spec bodyspec S on T = exprspec IsVip on Customer = tier == Gold
Operation bodyoperation o(...): T = exproperation discountRate(tier: LoyaltyTier): Decimal = ...
Read-model fieldname: Type = exprlineCount: Int = lines.count
Factory init / command transitionfield -> exprtotal -> lines.sum(l => l.price)
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 * taxRate

Operators bind from lowest precedence (top) to highest (bottom):

PrecedenceFormOperatorsAssociativity
1 (lowest)bindinglet … in …
2guardexpr when condnon-associative
3conditionalif … then … else …right (nests in else)
4coalesce??left
5logical or||left
6logical and&&left
7equality== !=left
8relational< <= > >=left
9matchmatches /…/non-associative (infix)
10additive+ -left
11multiplicative* /left
12unaryprefix ! -right
13postfix.member, .op(args)left
14 (highest)primaryliteral, name, ( … )

The atoms of every expression:

  • Numbers10, 0.9, 0.0 (a decimal literal carries no suffix in .koi; the emitter adds m for Decimal).
  • Strings"X", ", " (double-quoted).
  • Booleanstrue, 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 == InStock

Here availability is a field and InStock is a bare member of its enum — both are identifiers.

Boolean logic uses && (and), || (or), and prefix ! (not).

spec IsLargeOrder on Order = lines.count > 10 || total.amount > 1000
requires !lines.isEmpty "cannot submit an empty order"

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"
}

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.

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 lineTotal

It 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.0

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.

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.

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).

KoineMeaningEmitted C# (sketch)
xs.countelement countXs.Count
xs.isEmptyis emptyXs.Count == 0
xs.isNotEmptyis non-emptyXs.Count != 0
xs.all(l => p)every element satisfies pXs.All(l => p)
xs.any(l => p)some element satisfies pXs.Any(l => p)
xs.sum(l => e)fold e over elementsnumeric: Xs.Sum(l => e); value object: Xs.Select(l => e).Aggregate((a, b) => a + b)
xs.distinctBy(l => k)no two elements share key kXs.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;

String operations are written as member access on a String receiver. They chain left to right.

KoineMeaningEmitted C#
s.lengthcharacter counts.Length
s.trimstrip surrounding whitespaces.Trim()
s.upperupper-cases.ToUpperInvariant()
s.lowerlower-cases.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 ….

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:

KoineMeaningEmitted C#
a ?? bcoalesce — a if present, else b(a ?? b)
field.isPresenttrue when setfield is not null
field.isNonetrue when absentfield 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;

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 … in bindings → 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 / .isNonefield is not null / field is null.
  • Collection ops → LINQ (All, Any, Sum, Select, Aggregate, Count, Distinct).
  • String opsTrim(), ToUpperInvariant(), ToLowerInvariant(), Length.
  • Atomic token note-> and <-> are single, indivisible tokens; see §3.7 for the atomic-token rule.