Skip to content
You are reading v0.2, which is not the current release. Go to v0.4.

Types

Khora is statically typed with inference for ordinary local expressions and explicit annotations where a boundary or ambiguity needs one.

Bindings:

let count: Int = 42;
let names: List<String> = [];

Parameters and returns:

fn add(left: Int, right: Int) -> Int {
left + right
}

A named type is a path, optionally with generic arguments:

Int
String
app::model::User
List<User>
Result<User, DbError>

Compile-time path segments use ::.

()

() is both the unit type and unit value.

(Int, String)
(Int, String, Bool)
(Int,)

Parentheses without a comma group a type:

(Int)
{
id: Int,
name: String,
}

Record fields may be explicitly mutable:

{
mut count: Int,
name: String,
}

A mut field can be assigned through a value of that record type; an ordinary field cannot.

let seen: Tally = { name: "hits", count: 0 };
seen.count = seen.count + 1;

This is what in-place aggregation is written out of, and the fast shape for grouping by a small key — counters updated where they sit, rather than a new value per event:

let buckets: Array<Tally> = Array::from_fn(3, fn i => { name: "b${i}", count: 0 });
Array::get(buckets, 1).count = Array::get(buckets, 1).count + 5;

Array::from_fn and not Array::new: new puts the same value in every cell, which is invisible for a record nobody can change and is one counter with three names the moment a field is mut. from_fn calls its function per cell.

A record with a mut field cannot cross into a fiber. Two fibers writing one record is a data race, and the type says so; Sharing is what does cross.

| None
| Some(value: A)

A normal named declaration uses a variant type on the right-hand side:

pub type Option<A> =
| Some(value: A)
| None;

Payloads may be named:

| Point(x: Int, y: Int)

or positional:

| Pair(Int, String)

Unary function:

A -> B

Several parameters are represented by the parameter tuple on the left:

(A, B) -> C

Function arrows are right-associative:

A -> B -> C

means a function from A to a function from B to C.

Request -> Response with { db: Db }

Open capability row:

A -> B with 'ef

A function type can carry both requirements and failures:

Request -> Response
with { db: Db, clock: Clock }
raises DbError + ValidationError

The clauses belong to the arrow they follow.

One Unicode scalar value, thirty-two bits wide.

fn initial(name: String) -> Option<Char> { String::char_at(name, 0) }

Not a String of length one. A String can be empty or hold a thousand characters, so a function taking one has to decide what those mean; a Char cannot be either. It lives in a register, so scanning a string a character at a time allocates nothing.

A scalar value is not every 32-bit number. The range stops at 0x10FFFF, and the surrogates 0xD800 to 0xDFFF are a hole in the middle of it — they exist only to encode a pair in UTF-16 and are not characters. Char::from_code stops the program on either, the way U8::of does on a number that does not fit.

Char::code goes the other way and cannot fail.

+ joins the failure types of a raises row:

DbError + ValidationError + HttpError

For a declaration:

fn serve() -> Response
raises DbError + HttpError
{
// ...
}

Only in a raises or with clause. + builds a row, and a row is what those two clauses take. Written anywhere else it is refused:

fn hold(r: Result<Int, A + B>) -> Int // error

A Result holds one error type. Handle a wider row with catch, which matches per type and never has to name a combined type.

There is no union type — no way to write “an Int or a String” as the type of a value. + in a bound (T: Eq + Show) is the other meaning of the symbol and means the parameter implements both.

List<String>
Map<String, User>
Matrix<Float, 4, 4>

Type arguments can include integer literal types for const-generic parameters.

A row variable starts with ':

'ef
'er

They are used where an API is polymorphic over capabilities or failures:

A -> B with 'ef raises 'er

The name after ' has no built-in meaning. 'ef for capabilities and 'er for errors are conventions, spelled with two letters so they cannot be read backwards: 'e alone looks like “errors” to anybody arriving from a library that calls them that.

A closed capability-shaped row:

{ db: Db, clock: Clock }

A row with an open tail:

{ db: Db | 'ef }

Rows may merge additional row values in the tail position:

{ 'left | 'right | clock: Clock }

An error row names failure types rather than capabilities, and its entries are bare:

{ Oops }
{ Timeout | Refused }

The bare spelling above is the one a raises clause takes. In type-argument position — the one place a row has to be written down rather than inferred — an error row’s entries are labelled, and an error row labels each type with its own name. A Fiber’s second parameter is a row, so the form that works everywhere is:

fn stop(f: Fiber<(), { Oops: Oops }>) -> () { Fiber::cancel(f) }
let f: Fiber<(), { Oops: Oops }> = Fiber::spawn(work);

Fiber<(), Oops> is a type where a row belongs; it is refused, and the compiler prints the shape it wanted:

error: this argument: expected `() -> () raises { | Oops }`, found
`() -> () raises { Oops: Oops }`; `Oops` is a type and a row belongs
here. A row's entries are labelled, and an error row labels each type
with its own name — write it `{ Oops: Oops }`. The bare name is right in
a `raises` clause and only a type argument needs the braces

{ Oops } is not the same thing as { Oops: Oops }. In a let annotation it happens to check, because the annotation is unified against an inferred type. In a parameter or return type it is read as { | Oops } — an open-tail row variable named Oops — the declaration is accepted silently, and the mismatch surfaces at the call site with the caret on the argument rather than on the signature. Write the labelled form and it is right in both places.

Most code never writes one, because a signature’s raises clause takes the types directly (raises Oops) and everything else infers. Fiber<A, 'er> in the standard library and Fiber<(), 'er> in the concurrency reference are row variables, which need no braces — which is why the concrete form is easy to miss.

Rows are structural; ordinary declared ADTs remain nominal.

forall<A>. A -> A

Several parameters, including const parameters, use the normal generic parameter list:

forall<A, const N: Int>. Vector<A>

Most named generic functions do not need explicit forall; the declaration’s own type parameter list introduces their parameters.

A type declaration may omit its definition:

pub type Handle;

The representation is not available to ordinary source using the type.

A type declaration over an existing type gives a domain name to a value, and gives it a type of its own:

pub type UserId = Int;
pub type OrderId = Int;

These are distinct types, not other spellings of Int. A UserId is not accepted where an Int is wanted, an Int is not accepted where a UserId is, and a UserId is not an OrderId — which is the reason to write one:

error: this argument: expected `UserId`, found `OrderId`

Build one by calling the type’s name, and open it by matching on it, the shape a Rust tuple struct has:

let id = UserId(1);
fn number(id: UserId) -> Int {
match id { UserId(value) => value }
}

derive applies as it does to anything else, and is usually wanted: without Eq and Ord a UserId cannot be a Dict key, and without Show it cannot go in a ${..} hole.

derive(Eq, Ord, Show)
pub type UserId = Int;

Show prints UserId(1), not UserId::UserId(1) — the one case is the type.

The underlying type may be anything, a generic one included, which is how a long type gets a short name:

pub type Books = Dict<Currency, Bucket>;

Khora has no transparent alias: no form meaning “another spelling of the same type”. Where that is what is wanted, write the type out, or take the wrapper and the one match it costs.

The other two ways a declaration names data, for comparison — a record:

pub type User = {
id: UserId,
name: String,
};

and a variant:

pub type Lookup =
| Found(user: User)
| Missing;

See Declarations for visibility, derive, and complete declaration forms; see Generics for bounds, const parameters, variance, and polymorphism.

Khora 0.2.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource