Expressions
Khora is expression-oriented. Literals, calls, blocks, conditionals, matches, pipelines, lambdas, handlers, and control-flow forms all appear in expression position according to their type.
Literals
Section titled “Literals”421_000_0003.146.02e2319.99dtruefalse"hello"`multiline text`See Lexical structure for exact literal and interpolation forms.
Paths and names
Section titled “Paths and names”valueapp::model::UserResult::Ok:: is compile-time namespacing for modules, types, constructors, and associated items.
parse(input)connect(host, port)Arguments are positional and may have a trailing comma.
Runtime field projection and methods
Section titled “Runtime field projection and methods”user.nameresponse.statususer.display_name(). operates on a runtime value. It is distinct from :: path lookup.
Record literals
Section titled “Record literals”{ id: 42, name: "Ada",}A braced form beginning with name: is a record literal; an ordinary braced
sequence is a block.
{} in expression position is a record literal, not an empty block. It is
the value of a record type declared with no fields — pub type Nothing = {}; —
and it only checks where such a type is expected:
pub type Nothing = {};
let nothing: Nothing = {};Everywhere else it is refused, and the message says so and says what to write instead:
error: `{}` is an empty record literal, and no record type here is declared with no fields. Write `()` for a block that does nothing --> src/main.kh:9:21 |9 | Option::None => {}, | ^^The place this bites is a match arm that should do nothing. => is followed
by an expression, so Option::None => {} is a record literal and is refused.
Write Option::None => () for an arm that produces unit. A braced block after
=> is fine as long as it is not empty: Option::None => { retire(); }.
Record update
Section titled “Record update”{ ..base, field: value } builds a new record from an existing one. Every
field not named comes from base:
let renamed = { ..user, name: "Grace" };It is a new record: user is unchanged. What the syntax saves is writing
out the fields that do not change, which starts to matter at more than a few:
fn applied(counts: Counts, event: Event) -> Counts { match event { Event::Created => { ..counts, created: counts.created + 1 }, Event::Deleted => { ..counts, deleted: counts.deleted + 1 }, Event::Expired => { ..counts, expired: counts.expired + 1 }, }}The base comes first and appears once. A field named twice is an error rather
than last-one-wins, so is a field the base’s type does not have, and so is a
base that is not a record. { ..base } with nothing after it is base.
A mut field is the other way to do this. The update
produces a new value; assigning a mut field changes the one already held.
Reach for the update where the old value still matters, and for mut where it
does not.
Tuple and unit expressions
Section titled “Tuple and unit expressions”()(1, "one")(1,)Parentheses without a comma group an expression:
(value + 1)List literals
Section titled “List literals”[][1, 2, 3][ "Ada", "Grace",]Integer division and remainder
Section titled “Integer division and remainder”/ truncates toward zero and % takes the sign of the dividend, which is what C, Rust, Go and the hardware instruction all do:
(0 - 7) / 2 // -3, not -47 / (0 - 2) // -3(0 - 7) / (0 - 2) // 3
(0 - 7) % 2 // -17 % (0 - 2) // 1(0 - 7) % (0 - 2) // -1The two agree, so a == (a / b) * b + (a % b) holds for every pair that does not trap. Float::to_int truncates toward zero for the same reason, and says so.
Both trap on a zero divisor rather than answering anything — see Traps. For a quotient that rounds rather than truncating, do the rounding in Decimal, where the mode is a parameter and not a convention.
Blocks
Section titled “Blocks”{ let subtotal = 40; let tax = 2; subtotal + tax}The final expression without a semicolon is the block value. Statements before it are evaluated in order.
Local bindings
Section titled “Local bindings”let value = compute();let value: Int = compute();let mut count = 0;let (left, right) = pair;General form:
let mut? Pattern (: Type)? = Expr ;A binding is immutable unless it says mut, and a plain let cannot be
assigned to later. mut is fiber-local mutation only: state that several
fibers evolve is a Shared boundary instead, and
Sharing says why.
let is local. Module-level named expressions use const; see Declarations.
Assignment
Section titled “Assignment”count = count + 1Assignment is an expression of type () and is right-associative. The target must be writable, such as a let mut binding or a mutable record field.
Lambdas
Section titled “Lambdas”Single parameter:
fn value => value * 2Ignored parameter:
fn _ => fixed_valueSeveral or annotated parameters:
fn (left: Int, right: Int) => left + rightBlock body:
fn value => { let doubled = value * 2; doubled + 1}Pipeline |>
Section titled “Pipeline |>”First-argument insertion:
value |> transform(a, b)is equivalent to:
transform(value, a, b)One _ placeholder selects another argument position:
value |> transform(a, _, b)A stage may contain at most one placeholder.
Bare unary function:
value |> normalize |> validateFallible stage:
value |> parse! |> validate(config)!A pipeline introduces no second error model: ! still marks the exact call
where a typed failure may leave the function, and catch still applies either
to one stage or to the parenthesised pipeline as a whole.
let user = (raw |> parse! |> validate!) catch { ParseError::Invalid(message) => User::invalid(message),};Flow lambda ||>
Section titled “Flow lambda ||>”||> starts a unary anonymous pipeline:
||> normalize|> validate!|> persist!For example:
items |> List::map( ||> normalize |> validate!)It is equivalent in shape to fn value => value |> ..., and infers its
effects, failures and captures the same way.
Following |> stages belong to the flow lambda until grouping ends, so piping
the function value itself takes parentheses:
(||> normalize) |> apply_twice||> is always unary. An anonymous function of several parameters is fn.
A named function needs neither: items |> List::map(normalize).
Reach for a pipeline when the value moving through the expression is the thing to follow, and for an ordinary call when the operation itself is the point of the line.
Operators and precedence
Section titled “Operators and precedence”From loosest to tightest:
- assignment
=— right-associative - pipeline
|>— left-associative - logical OR
|| - logical AND
&& - comparisons
== != < > <= >= - addition/subtraction
+ -—+also joins twoStrings, which is the only concatenation operator there is; there is no++ - multiplication/division/remainder
* / % - prefix negation
-and boolean not! - postfix call, field access, failure
!,catch, andwith
Examples:
value + 1 |> doubleready && count > 0!enabled-totalPrefix !value is boolean negation. Postfix call()! marks failure propagation; position disambiguates them.
Failure postfix !
Section titled “Failure postfix !”load_user(id)!The inner call keeps its normal value type while its declared failure row is allowed to leave the current computation. See Failures.
load_user(id)! catch { UserError::NotFound(_) => User::guest(), UserError::Unavailable(reason) => User::offline(reason),}catch is postfix on the expression whose typed failures it handles, and that
expression still needs its !. The mark is what lets the failure leave the
call; catch only says what to do with it once it has. Omitting it is refused:
error: `load_user` can leave this function, so the call needs `!`: write `load_user(..)!`Postfix capability installation
Section titled “Postfix capability installation”load_user(id)! with { store: test_store,}A named context can be supplied the same way:
load_user(id)! with Productionwith overrides:
load_user(id)! with Production { store: test_store,}See Capabilities.
Handler expressions
Section titled “Handler expressions”handler for Clock { now: fn () => fixed_instant,}A handler expression produces a value implementing the named effect.
with blocks
Section titled “with blocks”with { clock: fixed_clock, store: test_store,} { run_job()!}Named context:
with Production { run_server()!}Control-flow expressions
Section titled “Control-flow expressions”The following forms are expressions or block-like expressions and have dedicated rules in Control flow:
if condition { a } else { b }match value { Pattern => result, }while condition { body; }for pattern in iterable { body; }loop { body; }breakbreak valuecontinuereturnreturn valueraise errorPatterns used by match, for, destructuring let, and catch are listed in Patterns.
Khora 0.2.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource