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;

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:

Targetbest: Decimal? = hint ?? fallback where hint: Int?, fallback: Decimal?
C#(Hint ?? Fallback) — an implicit intdecimal 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)
Kotlinthis.hint?.let { java.math.BigDecimal.valueOf(it) } ?: this.fallback
Javathis.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.

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.