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

std::core

The vocabulary every Khora program starts with.

Option and Result, the comparison and display traits, lists, arrays, strings, vectors, maps and dictionaries, and the pieces structured concurrency is built out of – regions, fibers, nurseries and Shared. Nothing here is optional in the way the rest of std is: two libraries that disagree about what an Option is cannot exchange one.

Khora tracks effects as rows on the signature rather than as a wrapper type, so there is no Effect<A, R, E> here. A computation’s requirements live in its with clause and its failures in its raises clause; both are inferred on private functions and written out on exported ones.

The module is std::core rather than std::effect for two reasons: there is no longer an Effect type to name it after, and effect is now a reserved word, so std::effect will not lex as a path.

Types: Ordering · Option · Result · Redacted · Validated · List · Halves · Parts · Step · Range · Never · Region · Fiber · Array · Split · Ptr · Chain · Pair · Least · Dict · Map · Vector · SharedFn · Changed · Channel · Shared · Fibers · ChildFailed

Traits: Eq · Ord · Hash · Show · Share · Iterator · Functor · Applicative · Traversable

Effects: Scope · Nursery

Methods: Bool · Char · Ordering · Option<A> · Result<A, E> · Redacted<A> · Validated<A, E> · List<A> · List<A> · List<A> · List<A> · List<Int> · Region · Scope · Fiber<A, 'er> · Array<A> · Int · String · Float · Int · Ptr · String · U8 · U16 · U32 · U64 · I8 · I16 · I32 · Dict<K, V> · Dict<K, V> · Map<K, V> · Vector<A> · SharedFn<A, B, 'er> · Channel<A> · Shared<A> · Fibers

Trait implementations: Show for Int · Show for Float · Show for Bool · Show for Char · Eq for Char · Ord for Char · Hash for Char · Show for String · Eq for Int · Ord for Int · Hash for Int · Eq for Bool · Eq for String · Hash for Bool · Hash for String · Ord for String · Eq for Ordering · Show for Ordering · Show for Option<A> · Eq for Option<A> · Ord for Option<A> · Show for Result<A, E> · Eq for Result<A, E> · Show for Redacted<A> · Iterator for Range · Show for List<A> · Eq for List<A> · Ord for List<A> · Iterator for List<A> · Functor for Option · Applicative for Option · Traversable for Option · Functor for List · Traversable for List · Eq for U8 · Ord for U8 · Show for U8 · Hash for U8 · Eq for U16 · Ord for U16 · Show for U16 · Hash for U16 · Eq for U32 · Ord for U32 · Show for U32 · Hash for U32 · Eq for U64 · Ord for U64 · Show for U64 · Hash for U64 · Eq for I8 · Ord for I8 · Show for I8 · Hash for I8 · Eq for I16 · Ord for I16 · Show for I16 · Hash for I16 · Eq for I32 · Ord for I32 · Show for I32 · Hash for I32 · Show for Pair<K, V> · Show for Dict<K, V> · Show for Map<K, V> · Show for Vector<A>

Functions: assert · print · todo · join_all · nursery · bounded_nursery · scoped · acquire · attempt

pub type Ordering =
| Less
| Equal
| Greater;

The result of comparing two values.

A three-way answer rather than two booleans: one comparison decides all six operators, and a type cannot accidentally disagree with itself about < and >=.

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

A value, or nothing.

For absence that is ordinary and expected: a key that is not in a map, the head of an empty list. When the absence is a failure — something went wrong and the caller should know what — the type is Result, and ok_or below is the bridge between them.

pub type Result<A, E> =
| Ok(value: A)
| Err(error: E);

A value, or the reason there is not one.

The same thing a raises row says, said as a value. A tagged return already is “an error or a value”; which of the two readings a function offers is a choice about its caller, and attempt is where one becomes the other.

pub type Redacted<A> = | Secret(value: A);

A value that a log line cannot get at.

A password, a token, an API key. The leak everybody actually has is not a clever one: it is a Config record in a structured log, or a ${} in a message written while debugging something else, and no review catches it because nothing about the line looks wrong.

Show prints <redacted>, and there is no Encode. Those two sentences are the whole design, and each is deliberate in a different direction:

  • With Show, a record holding one still derives Show, so the config a service prints at start-up stays printable and the secret in it does not appear. A Redacted with no Show at all would look stricter and be worse: it makes the containing record unprintable, and the way people answer that is by not wrapping the secret.
  • Without Encode, a record holding one does not derive Encode, and the build stops. That is the right place to stop: a type that serialises a secret is a bug, and one that serialises "<redacted>" is a payload that fails to round-trip somewhere further away. It still derives Decode, because a secret arrives from somewhere: it reads, and does not write.

This is where a nominal type earns its keep. Elsewhere the same idea is a toString override, which any structured logger walks straight past because it reads fields rather than calling it. Here it is a missing impl, which is a compile error.

derive(Show)
type Config = { host: String, password: Redacted<String> };
let settings: Config = { host: "db.internal", password: Redacted::of(secret) };
print(settings.show()); // Config { host: db.internal, password: <redacted> }
connect(Redacted::expose(settings.password));

Interpolation is covered by the same Show. "${key}" calls it like any other hole, so the line compiles and prints <redacted>.

Nothing needs expose to print one. expose hands over the secret itself, which is the shortest route into a log for somebody who only wanted their message printed; a hole that already redacts removes the reason to reach for it.

So every way a value reaches a log – show, a derived Show on something holding it, and a hole – goes through the one implementation, and there is no second thing to keep in step with it.

Deliberately no Eq. Comparing two secrets is a real thing to want and a derived Eq would compare them byte by byte and stop at the first difference, which is how a timing side channel gets written by somebody who was not writing one. Anybody who needs the comparison writes expose and says so.

derive(Eq, Show)
pub type Validated<A, E> =
| Valid(value: A)
| Invalid(errors: List<E>);

Every reason the input was wrong, rather than the first one.

Result is the right shape when the next step depends on the last: it stops, because there is nothing sensible to do with a value that is not there. Validation is the other shape. The fields of a form, the keys of a config, the columns of a row – these do not depend on each other, and reporting them one restart at a time is a person running the program five times to be told five things it knew the first time.

Applicative::map2 is the operation that distinguishes the two, and std::core’s own docstring on it has been pointing here since it was written: “Option gives up if either is None, and a validation type would collect both failures.”

let settings = Validated::map2(
integer("PORT"),
string("HOST"),
fn (port, host) => Settings { port: port, host: host },
);

Not an Applicative instance, though it is one in shape. The instance would need Validated<_, E> – the error parameter fixed and the value parameter free – and Khora has no partial application of a type constructor. The methods are the same functions under their own names, and the instance can arrive later without changing a call site.

pub type List<A> =
| Nil
| Cons(head: A, tail: List<A>);

A singly-linked list: the sequence with no capacity, no reallocation and no index.

Cheap at the front and linear everywhere else. Vector is the one to reach for when the work is indexing or appending; this is the one to reach for when the work is “each of these, in order”, which is most of it.

[a, b, c] builds one — the literal is Cons(a, Cons(b, Cons(c, Nil))) and needs List in scope, exactly as 0.01d needs Decimal.

pub type Halves<A> = {
left: List<A>,
right: List<A>,
};

A list split down the middle, for sort.

pub type Parts<A> = {
kept: List<A>,
rest: List<A>,
};

A list split by a question, for partition.

Named rather than reusing Halves, whose left and right say where a piece came from and not what is true of it. A caller of partition reads .kept and knows which side answered yes; reading .left they would have to go and look.

pub type Step<S, A> =
| Yield(state: S, item: A)
| Done;

One step of an iteration: the next item and the rest, or the end.

Khora has no mutable references, so Rust’s fn next(&mut self) -> Option<Item> is not expressible: there is no way to hand next something it may write through. An iterator hands back its successor instead. for desugars to this and reads the same.

This said “no mutable references and no mutable fields” until mut fields arrived – Map is built out of one, Vector out of two – and the second half stopped being true without the first half needing to change. It is the reference that is missing, not the mutation.

This is the allocation for pays per element. A Step is an ordinary heap object, so a for loop is about 3.6 times slower than the same loop written out – measured in bench/iteration. Unboxing is what would close that; desugaring for to a closure would not.

pub type Range = | Of(from: Int, to: Int);

Range::Of(from, to) yields from up to but not including to.

pub type Never;

The empty type. A function whose return type is Never does not return normally — it raises, diverges, or transfers control to a handler.

The bottom type, which means a Never branch takes the other branch’s type. if index < 0 { stop(index) } else { xs } is an A when xs is, including when A is a type the caller chose — the branch that cannot return has nothing to disagree with.

The name is the compiler’s, the way Int and String are, so it needs no import. This declaration is where it is documented and what an import naming it resolves against; nothing resolves to it. It read as an ordinary opaque type until recently, which left the solver’s bottom — the type a raise already has — sitting next to a name that could not reach it.

pub type Region;

A region, and the finalizers deferred to it.

The type is here rather than hidden because scoped is written in Khora and has to name it. Reference counted like everything else — which is the whole mechanism: releasing the last reference runs the finalizers, and the paths that release a binding are already the paths a region has to end on.

pub type Fiber<A, 'er>;

A running fiber, and the only way to wait for one or stop it.

Releasing a handle joins, so a fiber cannot outlive the binding that holds it — on every path out, including a raise passing through. Put one in a region and the region waits; put it in a block and the block does. That is where structured concurrency comes from, and it needed nothing of its own.

pub type Array<A>;

A fixed number of elements, side by side.

Fixed because growing is a library question and this is the primitive one is built out of: { mut items: Array<A>, mut len: Int } is a vector, and it can be written in Khora now that a field can be written.

Reading an element is a bounds check and a load — no call — because the layout is a contract with the code generator rather than something the runtime hides. What the runtime does own is allocation and release, both of which need the length at run time.

pub type Split = {
head: String,
rest: String,
};

What split_once found: the text before the separator, and everything after it.

pub type Ptr;

An opaque machine address: a C void *.

Not counted, not dereferenceable, and never a pointer into Khora’s own heap. It exists so a foreign library can hand back a handle — a FILE *, an SSL_CTX * — and be given it again later. Nothing in the language turns a Khora value into one, which is what makes a dangling Ptr impossible: every pointer that exists came from the other side, and its lifetime is that side’s business.

Lending a buffer across is the harder question and this type deliberately does not answer it.

pub type Chain<K, V> =
| Empty
| Node(key: K, value: V, rest: Chain<K, V>);

One bucket’s worth of entries: the keys that hashed to the same slot.

Separate chaining rather than open addressing, because a chain is an ordinary recursive ADT and needs no tombstones, no sentinel value and no second array saying which slots are live. It allocates per entry, which is the price, and it is the shape the language is actually good at.

pub type Pair<K, V> = {
key: K,
value: V,
};

One key and the value under it.

pub type Least<K, V> = {
key: K,
value: V,
rest: Dict<K, V>,
};

The smallest entry of a tree, and what is left without it.

pub type Dict<K, V> =
| Empty
| Node(size: Int, key: K, value: V, left: Dict<K, V>, right: Dict<K, V>);

An ordered map that is never written in place.

Map is the other one: a hash table that mutates its buckets, which is fast and is exactly why it cannot cross a fiber or go in a Shared. This is built out of ordinary variants, so it holds nothing writable and is shareable in the ordinary structural way — a Shared<Dict<K, V>> is how a cache several fibers use is spelled.

Persistent, so an insert gives back a new map and the old one is still there. The two share everything the insert did not touch, which is what makes that affordable: one path from the root is rebuilt, and the rest of the tree is the same objects with one more reference each.

Weight-balanced (Adams), so every operation is logarithmic in the size and the recursion here is bounded by the depth rather than by the length — the distinction that String::slice got wrong and paid for.

pub type Map<K, V> = {
mut buckets: Array<Chain<K, V>>,
mut count: Int,
};

A hash table: unordered, mutated in place, and the fast one.

Map mutates and Dict does not, which is the whole choice between them. Insert into a Map and the map you were holding has changed; insert into a Dict and you get a new one and the old is untouched. So a Map cannot go into a Shared and cannot cross into a fiber, and a Dict can.

Keys need Hash, and Hash requires Eq because the two have to agree — see the trait. Entries come back in no particular order, and not even a stable one: growing rehashes everything.

pub type Vector<A> = {
mut items: Array<A>,
mut len: Int,
mut wanted: Int,
};

A sequence that grows: an array, the part of it that is in use, and a reallocation when those two meet.

The container to accumulate into. Array<A> is fixed at the length it was allocated with, so a caller that does not know the count in advance cannot use one; List<A> grows, but at the front, one allocation and one pointer chase per element, and indexing it is a walk. This is contiguous, indexed in constant time, appended to in amortised constant time, and — the point of the whole exercise — a constant number of allocations for any number of elements.

Fiber-local, like Map and for the same reason: the mut fields are what make push cheap, and they are exactly what the sharing rules refuse to a second fiber. A sequence several fibers write is a Shared<List<A>>, which is slower and says so.

The obstacle, and what it cost to get around it wrongly. Array::new needs a value to fill with, and there is no value of an arbitrary A to be had: Vector::new() has nothing in its hands, and neither has with_capacity, whose whole purpose is to allocate before there is anything to put in it. The first version of this type answered that by holding Array<Option<A>> — every cell could then be Option::None — and that answer was wrong in a way worth recording, because it type checked, passed its tests and read perfectly well. It allocates a Some for every element. A hundred integers pushed into a vector were a hundred and three live objects, where they are two now, at that length and at every other: the vector and its array. Allocating per element is precisely the property a vector exists to not have, and it is the reason to reach for one over a List. The other way out is a new that takes a seed element, which is C++’s value-initialisation: it turns “give me somewhere to collect results” into “produce a result first”, and a type with no cheap value has to fabricate one.

The answer is Array::empty, which is a zero-length array and needs no fill because it has no cells. The storage starts there and costs nothing; the first push allocates for real, and it can, because the element being pushed is the value to fill with. Every cell of a fresh array holds that element until a later push overwrites it, which for a boxed A is one reference count per cell rather than one allocation per cell, and for an Int is nothing at all.

with_capacity is the field that falls out of this: it cannot allocate when it is called, so it records what was asked for in wanted and the first push honours it. A capacity is a promise here, not storage — capacity answers with what is really allocated, so a vector that has been asked for a thousand cells and pushed to zero times still reports nought. The alternative, reporting the promise, would be a number no memory corresponds to.

What this costs against the Option cells is eager release. There is no “empty” to write into a cell a pop vacated, so pop writes the vector’s first element there instead — a value of A it is guaranteed to have — which releases the popped one just as promptly and costs a reference count. Popping the last element and clear both drop the array outright, which releases everything at once and is why neither keeps the storage; wanted remembers the size, so refilling reallocates exactly once.

mut wanted: Int

How big the first allocation should be. Meaningless once there is one.

pub type SharedFn<A, B, 'er>;

A function two fibers may hold.

A closure is never shareable on its own: what it captured is not in its type, so no amount of looking at (Request) -> Response says whether the thing behind it can be written. That is fine until a container of closures has to cross — a router holding its handlers, a table of callbacks — at which point the whole container is stuck on this side.

This is the proof, reified. SharedFn::of accepts only a closure written at the call, where the captures are on the screen and can be checked; what it returns has forgotten how it was made, and is shareable because the check already happened. A Route holding one is shareable in the ordinary structural way, with nothing special said about routers.

The cost is the wrapper at the mount site, and that is the honest price: the alternative is a bit in every function type, which colours every container of a function all the way up.

pub type Changed<A, B> = {
state: A,
result: B,
};

A new state, and something to hand back that is not the state.

What Shared::modify asks its change function for. A record rather than a tuple because a tuple literal has no expression form yet, and named halves read better than positional ones at the call anyway.

pub type Channel<A>;

A bounded queue one fiber puts values into and another takes them out of.

Shared<A> is a cell; this is a hand-off. The two answer different questions and the difference is what a value is while it is in flight. A cell holds one value that anybody may read or replace. A channel holds a queue of values that each go to exactly one taker, and — the part that matters — a fiber waiting at either end gives its worker back rather than spinning or blocking a thread.

Three things, and they are the same thing seen from different sides.

Owning a resource that cannot be shared. A handler may not capture anything writable, and a database connection is writable and strictly serial — two fibers writing one socket interleave their frames. So one fiber owns the connection and the others send it requests. The handler captures the channel, which is Share, and never the connection, which is not. This is why the type exists: packages/postgres could not be finished without it.

Backpressure. A full channel stops the sender until a taker catches up. That is not a limitation to be worked around; it is how a service under more load than it can serve declines work at the edge instead of queueing it until memory runs out.

A pool. A channel holding the four idle workers is the pool: taking one is a receive, giving it back is a send, and waiting for one is what the channel already does.

close says nothing more will be sent and releases everybody waiting. A receive then drains what is already queued before answering None, because values already sent are still worth having. Closing twice is allowed: the fiber that owns a channel and the fiber that finishes with it are often different code, and neither should have to know which went first.

No rendezvous. bounded(0) gets a capacity of one rather than a channel where a send waits for a receive. Zero is a useful thing and it is not this thing; building it here would mean a sender waiting on a receiver that is waiting on a sender, and getting that wrong is a hang rather than an error.

No select. Waiting on the first of several channels wants a primitive of its own, and nothing has needed one yet.

pub type Shared<A>;

A value two fibers may both change.

The sharing rules refuse a mutable record to a second fiber on purpose, and refuse a handler that captures one, because two fibers writing one value is a race. This is what they refuse them until: a cell holding a value, with the reads and writes serialized.

Mutation becomes replacement. There is no borrow of the inside — a counter is Shared::update(count, fn n => n + 1), not count.n = count.n + 1 — and that is the whole of why this is safe rather than careful. Handing out the inner value would mean stopping it escaping the critical section, and without lifetimes there is no way to say that. Nothing unshareable goes in or comes out, so there is nothing to leak.

The contents must be Share for the same reason, which also means a Map cannot go in one: it mutates its buckets in place. Dict is the ordered persistent map, and is what a shared table is made of.

pub type Fibers;

A nursery: the fibers started under it, and the guarantee that none of them outlives it.

Releasing one cancels its children and waits for them, which is where structured concurrency comes from — a fiber cannot escape the block that started it, because the binding holding the nursery ends on every path out. nursery is the ordinary way to get one.

One value means both “wait for the children” and “the answers are no longer wanted”, without ever being told which of the two it is: on the ordinary path nursery has already waited, so the release finds nothing left to stop.

pub type ChildFailed = { children: Int };

Raised by a nursery when one or more of its children ended with an error.

Not the child’s own error, because there is no such single thing. A nursery holds Fiber<(), 'er> with the row quantified per adoption, so two children may fail with two unrelated types and the nursery has nowhere to put either — Nursery says why. What survives the handles is the count, so that is what this carries.

A child that a nursery cancelled is not a failure and is not counted: cancellation is what a nursery does to its children on the way out, and counting it would make every early exit look like a fault.

match attempt(fn () => nursery(gather)!) {
Result::Ok(answer) => answer,
Result::Err(failure) => retry_smaller(failure.children)!,
}
pub trait Eq

Whether two values of a type are the same value.

Structural, and written rather than derived for a type whose identity is not its contents. Nothing here defines !=: one operator is one thing to get right, and the negation is the negation.

Float is not an instance, because NaN == NaN is false and a type whose values are not equal to themselves breaks every container built on this one. The numbers design note has the argument.

fn eq(self, other: Self) -> Bool

Whether these are the same value.

pub trait Ord: Eq

Ordering refines equality: anything orderable can be compared for equality, so Ord requires Eq rather than repeating it.

fn cmp(self, other: Self) -> Ordering

Where self falls relative to other.

Must agree with eq: cmp answering Equal and eq answering false is a sort that loses elements and a search that cannot find them.

pub trait Hash: Eq

A value that can be a key.

Requires Eq rather than repeating it, because the two have to agree: a map finds a key by hashing it to a bucket and then comparing, so equal values must hash equal. Unequal values may collide — that is what the bucket’s chain is for — but two values that eq says are the same and hash sends to different buckets are an entry that can be inserted and never found.

Float is deliberately not an instance, because it is not an instance of Eq: NaN == NaN is false, and a key that is not equal to itself is a lost entry in every language that allows one.

fn hash(self) -> Int

A number to bucket by. Any number: spreading it out is the map’s job, not this one’s, so an implementation only has to be consistent.

pub trait Show

A value rendered for a person to read.

Not a serialization format. show is for a log line, an error message and a test’s failure output; std::json is for something that has to be read back. Nothing promises that show round-trips, and Float is the one place it happens to — see the compatibility design note, which makes that a promise about Float rather than about Show.

fn show(self) -> String

This value, as text.

pub trait Share

Two fibers may hold this at once.

A marker: no methods, and implementing it asserts rather than provides. Nothing has to write it for an ordinary type — a record, a variant, a tuple is shareable exactly when everything inside it is, and the compiler can see that. This is for the types it cannot see inside: one declared with no body, whose behaviour lives in the runtime or across the C ABI.

Those default to unshareable, which is the only safe way round. Array can be written through Array::set and Ptr points at memory this language did not allocate; both look like empty structures and neither is safe for two fibers to share. So the author says which is which, and the reader can see where the assertion was made.

Fibers are operating-system threads today, so this is Sync rather than Send: both fibers keep their reference, and neither hands it over.

pub trait Iterator

Something a for loop can walk.

One method, and it hands back a successor rather than mutating: see Step above for why that shape rather than Rust’s. Implementing it is what makes a type usable in for.

type Item;

What the iteration yields.

fn next(self) -> Step<Self, Self::Item>

The next item and the iterator that follows it, or Done.

pub trait Functor

A container whose contents can be replaced one for one.

Higher-kinded: Self is applied to a type here rather than being one, which is what lets a single map serve Option, List and everything else with a shape. docs/vision.md names this as a non-negotiable, and it is the thing Rust’s trait system cannot say.

fn map<A, B, 'ef, 'er>(self: Self<A>, f: (A) -> B with 'ef raises 'er) -> Self<B> with 'ef raises 'er

Every element replaced by f of it, in the same shape.

f may fail and may need capabilities, and the rows say so rather than the signature quietly forbidding it. Without them the only way to map a fallible function over a container was to write the walk out by hand, which for a language whose headline is typed failure is the collection library and the failure system declining to compose.

Variables, so nothing that maps a pure function has to change: they instantiate empty and the call site keeps its shape.

pub trait Applicative: Functor

pure lifts a value; map2 combines two containers. map2 rather than ap because ap would need a container of functions, and reading a signature is easier than reading Self<(A) -> B>.

fn pure<A>(value: A) -> Self<A>

One value, in the smallest container that can hold it.

fn map2<A, B, C>(self: Self<A>, other: Self<B>, f: (A, B) -> C) -> Self<C>

Two containers combined element-wise by f.

What “combined” means is the instance’s to decide: Option gives up if either is None, and a validation type would collect both failures.

pub trait Traversable: Functor

Walks a structure left to right, collecting the effects of each step.

This is the abstraction Rust cannot express and the reason higher-kinded types are a non-negotiable in docs/vision.md: one traverse per container serves every Applicative.

fn traverse<A, B, F: Applicative>(self: Self<A>, f: (A) -> F<B>) -> F<Self<B>>

f over every element, with the containers turned inside out.

A List<A> and an (A) -> Option<B> give an Option<List<B>>: one answer for the whole walk rather than a list of answers. That inversion is the whole point — it is how “all of these, or the first reason not” is written once instead of per container.

pub effect Scope {
defer: (() -> ()) -> (),
}

Resource lifetimes.

A finalizer registered with a scope runs when the enclosing region ends, however it ends: falling off the end, an early return, or a raise passing through.

The operation takes only a thunk. A handler’s fields are ordinary closures and a closure is monomorphic, so an operation cannot be generic — and it need not be: acquire below is an ordinary generic function that closes over the value and the release, which is where the polymorphism belongs.

defer: (() ->()) ->()

pub effect Nursery {
adopt: (Fiber<(), 'er>) -> (),
}

Where a spawned fiber goes.

The operation takes a fiber rather than a thunk, because a thunk cannot be forwarded to Fiber::spawn at all. A fiber’s body has to be written where it starts, so that what it closes over can be checked against the rule that a mutable value may not cross — a thunk that arrived from somewhere else took its captures with it, and that is the one thing the check cannot see.

The answer is fixed at () because an operation cannot be generic in a type, and because a nursery has nothing to do with an answer it cannot hand back. A fiber whose result matters is one you keep the handle to and join.

The row is not fixed, and that is worth a sentence because it is the one place an operation is polymorphic. 'er here is quantified per call rather than per handler: two children raising two different things are both adopted by one nursery. It costs nothing at run time — a capability crosses as evidence and an error as a tag, so the handler’s closure is the same code for every row, which is exactly why a row can be generic where a type cannot.

So the idiom is one call inside another, and the body is on the screen:

nursery.adopt(Fiber::spawn(fn () => analyze(id)!));

adopt: (Fiber<(), ’er>) ->()

impl Bool
pub fn to_string(self) -> String

"true" or "false".

The same text show gives, under the name the other scalars use – Int::to_string and Float::to_string are both here, and a reader who has met those goes looking for this one. Show for Bool is written in terms of it rather than the other way round, so there is one place the two words are spelled.

impl Char

One Unicode scalar value.

Not a String of length one. A String can be empty or a thousand characters, so a function taking one has to decide what those mean; a Char cannot be either. It is thirty-two bits and lives in a register, which is the other half: scanning a string a character at a time through one-character Strings allocates once per character.

A scalar value, which is not every 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] is checked for both.

Written 'a', '\n', '\u{1F600}'. The escapes are the string ones.

pub fn code(self) -> Int

The code point, as an Int. Every Char has one, so this cannot fail.

pub fn from_code(value: Int) -> Char

The character with this code point, or a stopped program.

Stops rather than raises, for the reason every other narrowing conversion in this library does: a number that is not a character is a mistake in the program rather than a condition to handle, and U8::of sets the precedent. Check the range first if the number came from outside.

pub fn to_string(self) -> String

This character as a one-character string.

The UTF-8 encoding, written out here rather than asked of the runtime: four branches on the code point and String::from_bytes at the end, so the standard library’s own text handling has no intrinsic under it that a reader cannot follow.

pub fn utf8_length(self) -> Int

How many bytes this character takes in UTF-8: 1, 2, 3 or 4.

pub fn is_digit(self) -> Bool

'0' to '9'. ASCII only, and the name says so nowhere else, so: this is not Nd. Arabic-Indic and Devanagari digits are digits to Unicode and not to this, because a parser that accepted them would then have to decide what Int::of_string does with them.

pub fn is_alpha(self) -> Bool

'a' to 'z' and 'A' to 'Z'. ASCII only, for the reason on [is_digit].

pub fn is_alphanumeric(self) -> Bool

A letter or a digit, both in the ASCII sense above.

pub fn is_whitespace(self) -> Bool

Space, tab, newline, carriage return.

pub fn to_upper(self) -> Char

'A' for 'a', and everything else unchanged. ASCII only.

pub fn to_lower(self) -> Char

'a' for 'A', and everything else unchanged. ASCII only.

impl Ordering
pub fn is_less(self) -> Bool

Whether the comparison came out Less.

pub fn is_equal(self) -> Bool

Whether it came out Equal.

pub fn is_greater(self) -> Bool

Whether it came out Greater.

pub fn reverse(self) -> Ordering

a.cmp(b).reverse() sorts descending without a second comparison.

impl<A> Option<A>
pub fn is_some(self) -> Bool

Whether there is a value.

For a condition. Reaching for the value afterwards means matching twice; match once instead.

pub fn is_none(self) -> Bool

Whether there is not.

pub fn unwrap_or(self, fallback: A) -> A

The value, or fallback if there is none.

fallback is evaluated either way. unwrap_or_else takes a closure for when producing it is expensive.

pub fn unwrap_or_else<'ef, 'er>(self, fallback: () -> A with 'ef raises 'er) -> A with 'ef raises 'er

The value, or the result of calling fallback.

The closure is called only when there is nothing, which is the whole difference from unwrap_or — reach for this when producing the fallback costs something.

pub fn ok_or<E>(self, error: E) -> Result<A, E>

Turns absence into a failure, which is how an Option joins a raises chain.

pub fn and_then<B, 'ef, 'er>(self, f: (A) -> Option<B> with 'ef raises 'er) -> Option<B> with 'ef raises 'er

The next step, when it can also come to nothing.

map with a function that returns an Option gives an Option<Option<B>>, which nobody wants and everybody writes once. This flattens as it goes.

What its absence costs is a shape, not a keystroke. Reading a record out of positional fields is one lookup that may fail per field, and without this each is a match inside the previous one – examples/link_shortener nested six deep before this existed.

pub fn filter<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Option<A> with 'ef raises 'er

The value if it is there and wanted says so, and nothing otherwise.

The bridge from a predicate back into the chain: find gives an Option, and asking a second question about what it found should not mean leaving the chain to ask it.

impl<A, E> Result<A, E>
pub fn is_ok(self) -> Bool

Whether this succeeded.

pub fn is_err(self) -> Bool

Whether it did not.

pub fn unwrap_or(self, fallback: A) -> A

The value, or fallback if there was an error.

The error is discarded. That is the right thing exactly when the caller has already decided the reason does not change what they do; when it might, match.

pub fn ok(self) -> Option<A>

Discards the error, which is the one direction that cannot fail.

pub fn map_err<F, 'ef, 'er>(self, f: (E) -> F with 'ef raises 'er) -> Result<A, F> with 'ef raises 'er

Rewrites the error and leaves the value alone.

What a function does at the edge of its own vocabulary: a caller should see the failure in terms of what they asked for, not in terms of whatever was reached to answer it.

pub fn map<B, 'ef, 'er>(self, f: (A) -> B with 'ef raises 'er) -> Result<B, E> with 'ef raises 'er

Rewrites the value and leaves the error alone.

The other half of map_err, and having only that half was the odd part: a reader who has met one goes looking for this and finds a match.

Not Functor, which takes one type parameter and Result has two. An impl Functor for Result<_, E> needs partial application of a type constructor, which this language does not have and does not need for one method.

pub fn and_then<B, 'ef, 'er>(self, f: (A) -> Result<B, E> with 'ef raises 'er) -> Result<B, E> with 'ef raises 'er

The next step, when it can fail the same way.

The error type is shared, which is what makes this a chain rather than a pair of unrelated failures: a step that fails differently says so with map_err first, and the conversion is visible at the place it happens.

impl<A> Redacted<A>
pub fn of(value: A) -> Redacted<A>

Wraps a value so that showing it shows nothing.

pub fn expose(self) -> A

The value back.

The only way out, and it is meant to be visible. expose at a call site is a word a reviewer can search for; the leak this type exists to stop is the one nobody wrote on purpose.

impl<A, E> Validated<A, E>
pub fn of(value: A) -> Validated<A, E>

A value that was fine.

pub fn error(e: E) -> Validated<A, E>

One thing wrong.

pub fn is_valid(self) -> Bool

Whether nothing was wrong.

pub fn map<B>(self, f: (A) -> B) -> Validated<B, E>

The same value, transformed. Failures pass through untouched.

pub fn map2<B, C>(self, other: Validated<B, E>, f: (A, B) -> C) -> Validated<C, E>

Two together, keeping both sides’ failures.

This is the whole type. f runs only when both succeeded; when either did not, the answer carries every error from both, left to right, and f is not called. Chained through a record’s fields it reports the whole form in one pass.

pub fn zip<B>(self, other: Validated<B, E>) -> Validated<(A, B), E>

Both values together, keeping both sides’ failures.

map2 with the pair as its answer, and the one combinator that composes without an arity: zip(a, zip(b, c)) is three values in a nested tuple, and let (x, (y, z)) = t; takes it apart, however many there are. It is what a generated record decoder chains through, which is why there is no map6 and never will be.

pub fn map3<B, C, D>(self, b: Validated<B, E>, c: Validated<C, E>, f: (A, B, C) -> D) -> Validated<D, E>

Three together, keeping every side’s failures.

map2 stopped at two and a CSV row has five columns. Building a record out of four validated fields meant nesting map2 and carrying a tuple through it, or hand-rolling the accumulation – which two of the four review programs did, separately.

Errors come back left to right, whichever sides failed, and f runs only when every side succeeded. Written out rather than built from map2 through a chain of pairs: that version works and reads p.key.key.value by the time it reaches five.

pub fn map4<B, C, D, F>(self, b: Validated<B, E>, c: Validated<C, E>, d: Validated<D, E>, f: (A, B, C, D) -> F) -> Validated<F, E>

Four together, keeping every side’s failures.

pub fn map5<B, C, D, F, G>(self, b: Validated<B, E>, c: Validated<C, E>, d: Validated<D, E>, e: Validated<F, E>, f: (A, B, C, D, F) -> G) -> Validated<G, E>

Five together, keeping every side’s failures.

Five is where this stops, because a CSV row is what asked for it and a sixth field is a record built out of two of these. List::traverse does the unbounded version and would have done the whole job – except that Validated is deliberately not an Applicative, for the reason the type gives above, so the one type in std built for accumulating errors cannot use it.

pub fn and_then<B>(self, f: (A) -> Validated<B, E>) -> Validated<B, E>

A second step that can fail, run only if the first did not.

Fails fast, and that is not a contradiction. map2 collects because its two sides are independent; this one’s second step is written in terms of the first’s value, so there is no second answer to collect when there is no first value to write it against. Parsing is the shape: integer reads the text and then parses it, and “not set” and “not a number” are never both true of one variable.

pub fn to_result(self) -> Result<A, List<E>>

The failures collapsed into one Result, for joining a raises chain.

List<E> rather than E, because the point of getting here was that there may be more than one and throwing the rest away at the boundary would undo the collecting.

pub fn unwrap_or(self, fallback: A) -> A

The value, or fallback if anything was wrong.

impl<A: Ord> List<A>
pub fn sort(self) -> List<A>

The same elements, in order.

A merge sort, because a linked list is what it was invented for: it needs no indexing, no copying into an array and back, and it is stable — equal elements keep the order they were given, which is the property a caller notices the absence of when sorting by one field and expecting the previous sort to survive.

A: Ord rather than a comparison function, so that < on the elements and sort on the list cannot disagree. Sorting by something else is sort_by, which does not exist yet and wants a closure.

Its own recursion is the divide, so it goes about log2(n) deep — twenty frames for a million elements. The merge underneath it is iterative and so is the release of the intermediate lists, so neither adds a frame per element and the depth is the only depth there is.

pub fn split(self) -> Halves<A>

The first half and the second, each in the order it was given.

By counting to the middle, not by dealing alternately. Dealing is one pass and needs no length, and it is what this did first — but it is not stable, and the test said so. Two equal elements at positions 1 and 2 land in different halves with the later one on the left, and a merge that breaks its tie towards the left then puts them back the wrong way round.

The cost is a length and a reversal, both linear, against a sort that is already linearithmic.

pub fn min(self) -> Option<A>

The smallest element, or nothing if there are none.

Option rather than a fallback, because the smallest of nothing is not a number a caller can be handed – and a library that picked one would be picking for whoever forgot the list could be empty.

Ties go to the first, which matches sort being stable: min and head(sort(xs)) name the same element.

pub fn max(self) -> Option<A>

The largest element, or nothing if there are none.

Ties go to the first as well, for the same reason.

impl<A> List<A>
pub fn is_empty(self) -> Bool

Whether there is nothing in it.

Constant time, which length() == 0 is not.

pub fn length(self) -> Int

How many elements there are.

Walks the list. Nothing here caches it, so a loop that asks each time round is quadratic — take it once.

pub fn head(self) -> Option<A>

The first element, or None for an empty list.

pub fn push(self, item: A) -> List<A>

Prepends, which is the cheap end. Appending walks the whole list, so it is spelled concat and its cost is visible at the call site.

pub fn concat(self, other: List<A>) -> List<A>

self followed by other.

Copies self and shares other, so the cost is the length of the left argument. Building a list by repeatedly concatenating onto the end is the quadratic way to do it; push and reverse once.

pub fn reverse(self) -> List<A>

The same elements, back to front.

pub fn reverse_onto(self, acc: List<A>) -> List<A>

self reversed, in front of acc.

The accumulating half of reverse, and useful on its own: building a list front-to-back and reversing it at the end is the linear way to do what concat in a loop does quadratically. This is the “at the end”.

pub fn fold<B, 'ef, 'er>(self, start: B, step: (B, A) -> B with 'ef raises 'er) -> B with 'ef raises 'er

Combines every element into one value, left to right.

step is called with what has been accumulated so far and the next element. Summing is fold(0, fn (a, b) => a + b).

pub fn any<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Bool with 'ef raises 'er

Whether any element satisfies wanted.

Stops at the first one, which is what makes it worth having over a fold: a fold visits everything, and the whole point of asking whether any element matches is that the answer often arrives early.

pub fn all<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Bool with 'ef raises 'er

Whether every element satisfies wanted. Stops at the first that does not.

True of the empty list, which is the convention that makes all compose: splitting a list in two and asking each half gives the same answer as asking the whole, and that fails at the empty case under any other rule.

pub fn find<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Option<A> with 'ef raises 'er

The first element satisfying wanted, or nothing.

pub fn take(self, count: Int) -> List<A>

The first count elements, or all of them if there are fewer.

Below one is empty rather than an error: take is what a caller reaches for when the count came from somewhere else, and refusing zero would make every such caller check it first.

pub fn drop(self, count: Int) -> List<A>

Everything after the first count elements.

pub fn nth(self, index: Int) -> Option<A>

The element at index, counting from zero, or nothing.

pub fn last(self) -> Option<A>

The last element, or nothing.

pub fn filter<'ef, 'er>(self, keep: (A) -> Bool with 'ef raises 'er) -> List<A> with 'ef raises 'er

The elements keep accepts, in the order they were in.

Inherent rather than a trait method, unlike map: Functor says how to change what is in a container, and dropping elements changes the container itself, which is why no lawful Functor has a filter. There is a class that does – Filterable – and one method is not a reason to introduce one.

filter then map is two walks; fold is one, and is what to reach for when that matters.

pub fn sort_by(self, order: (A, A) -> Ordering) -> List<A>

The same elements, in the order order asks for.

What sort cannot do, and its doc comment already said so: sort takes A: Ord on purpose, so that < on the elements and sort on the list cannot disagree. Sorting a record by one of its fields is a different question, and this is where it is asked.

The same merge sort and the same guarantees: stable, so equal elements keep the order they were given – which is what makes sorting twice, by one key and then another, do what everybody expects – and about log2(n) frames deep rather than one per element.

order must be consistent: if it says a is less than b it must not also say b is less than a. One that is not produces some order and no crash, which is the most any sort can promise about a broken comparison.

pub fn partition<'ef, 'er>(self, wanted: (A) -> Bool with 'ef raises 'er) -> Parts<A> with 'ef raises 'er

The elements wanted accepts and the ones it does not, in one walk.

Two filters with opposite predicates is the alternative, and it walks twice and calls the question twice per element – which is wrong when the question is expensive and wrong in a different way when it is not pure. Both sides keep the order they were given.

impl<A: Eq> List<A>
pub fn contains(self, wanted: A) -> Bool

Whether wanted is in the list.

A: Eq rather than a predicate, because contains is the question somebody asks about a value they are holding – List::any is already there for the predicate.

impl<A> List<A>
pub fn flat_map<B, 'ef, 'er>(self, step: (A) -> List<B> with 'ef raises 'er) -> List<B> with 'ef raises 'er

Each element’s list, laid end to end.

map and then a flatten, written as one pass because the intermediate list of lists is the only thing the two-step version allocates that nobody wants.

pub fn zip<B>(self, other: List<B>) -> List<Pair<A, B>>

Pairs, one from each, stopping at the shorter.

Stopping rather than padding, because there is nothing to pad a List<A> with: A is any type and this library has no notion of a default. A caller who wants the tail of the longer one still has it.

impl List<Int>
pub fn sum(self) -> Int

Every element added together. Empty is zero.

Int only, and deliberately: a sum over anything addable wants a Monoid, and std::core has Functor and Applicative and not that one yet. Naming the concrete case is honest; a half-general one would have to be taken back.

impl Region
pub fn open() -> Region

A new region with nothing deferred into it.

scoped is the way to reach one from ordinary code; this is what scoped calls. Opening one by hand means being responsible for the binding that ends it.

The region ends with the block the binding is in, not with the function and not at the next statement. So

{
let region = Region::open();
Region::defer(region, fn () => print("released"));
work();
};

releases before the statement after the block, on every way out of it including a cancellation. Putting the let at the top of a function therefore holds everything until the function returns, which is the difference between a lease that ends with the call and one that ends with the caller – packages/postgres had exactly that bug, and a pool of n behaved like a pool of n uses.

pub fn defer(self, finalizer: () ->()) ->()

Registers finalizer to run when this region ends.

Reverse order: the last deferred is the first run, so a finalizer may depend on one registered before it — a transaction rolled back before the connection it ran on is closed. Cleanup runs with cancellation held off, so a finalizer caused by a cancellation is not cut short by it.

pub fn root() -> Region

The region that ends when the program does, released by the entry point after main returns — on the failing path as well as the ordinary one.

impl Scope
pub fn root() -> Scope

The outermost scope: the one that ends when the program does.

An entry point has to get a Scope from somewhere, and there is nothing above it to ask. Everything else should take one rather than reach for this — a library that roots its own scope has decided its caller’s lifetimes for it.

impl<A: Share, 'er> Fiber<A, 'er>
pub fn spawn(body: () -> A raises 'er) -> Fiber<A, 'er>

Runs body on a fiber of its own.

The thunk may fail, and a thunk that can is also one that can be stopped: a cancellation travels out on the same tagged return an error does, so a fiber with no error row has no channel to be interrupted on and runs to its end.

A must be Share for the reason every value crossing a fiber must be: it is computed on one and read on another, so a thing that cannot be held twice cannot be an answer.

pub fn join(self) -> A raises 'er

Waits for the fiber to finish, and answers what it answered.

A failure comes back out here, with its type. If the fiber raised, so does this — the whole point of a fiber that returns a value is that the caller decides what a failure means, rather than the runtime printing a line nobody reads. Joining twice is joining once, and gets the answer twice.

'r is the row the body raised, carried on the handle. That is what makes this pleasant rather than merely possible:

let count = Fiber::join(Fiber::spawn(fn () => tally(rows)));
// no `!`, it cannot fail
let row = Fiber::join(worker)! catch {
DbError::Timeout => Row::empty(), // by name, because the row is here
};

An erased handle would have needed a ! on every join — including on a fiber that provably cannot fail — and catch { _ => .. } as the only arm. The effect-survey design note §3.1 records what that cost and why the row is on the type instead.

pub fn wait(self) ->()

Waits for the fiber, and does not take its answer.

join is for the answer; this is for the ordering. They are not the same wait, and the difference shows up on a fiber that was cancelled: a cancelled fiber has no answer, so a join has nothing to hand back and unwinds the joiner along with it. This one just waits. Reach for it when what you needed was “not before that finishes”, which is most of the time a fiber is spawned for what it does rather than for what it computes.

Letting the binding go waits too — that is where structured concurrency comes from — so this is for the case where the waiting has to happen at a particular line rather than at the end of a scope.

pub fn cancel(self) ->()

Asks the fiber to stop at its next cancellation point. Returns at once.

pub fn detach(self) ->()

Stops waiting for the fiber, and asks it to stop.

The valve, and there has to be one. Every other way out of a handle waits: join waits, and so does letting the binding go, which is where structured concurrency comes from. That is right, and it is also how a program hangs — one finalizer that never returns holds its nursery, which holds its parent, up to main. The scheduler design note promises both bounded cancellation latency and that a nursery exit leaves every child stopped or joined, and those two are in tension exactly here.

So: signal, and go. The fiber keeps running, its answer is dropped when it arrives, and a failure it reports afterwards is silent — the program said it was no longer listening. Without this, a timeout over a body with an uninterruptible tail is a lie: it would promise to return in half a second and then block on the tail.

It cancels as well as detaching, because a detached fiber nobody asked to stop is a leak with a nicer name.

impl<A> Array<A>
pub fn empty() -> Array<A>

No elements at all.

new needs a value to fill with and a generic container has none to give until something has been put in it, so without this there is no way to write down an empty Array<A> — Array::new(0, fill) still demands the fill it will never write. That gap is what Vector<A> is built on: its storage starts here and is allocated for real by the first push, which is also the first moment there is a value of A to fill with.

pub fn new(length: Int, fill: A) -> Array<A>

length elements, every one of them fill.

Every one of them the same fill, which matters when it can be written. A record is a value and two cells holding one that nobody can change are indistinguishable from two copies – but give it a mut field and they are one object with two names:

type Agg = { mut count: Int };
let slots: Array<Agg> = Array::new(3, { count: 0 });
Array::get(slots, 0).count = 1;
Array::get(slots, 1).count = 1;
// every slot now reads 2

An array of mutable accumulators is the natural way to group by a small key and it is the fast way – thirteen nanoseconds an event – so this is a shape people reach for. It reports counts multiplied by the number of buckets, silently, with nothing to say so. Use [Array::from_fn] below, which builds each cell separately.

The one place std disagrees with its own naming rule. new means “an empty one of something that grows”, and an array does neither – Array::empty sits above this for the empty case. Array::filled(length, fill) is what the rule asks for, and renaming a published std signature is a compatibility decision rather than a style one. The naming design note records it.

pub fn length(self) -> Int

How many elements it has. Fixed at construction; nothing grows an array.

pub fn get(self, index: Int) -> A

The element at index.

An index outside the array stops the program and says which index and what length. Same reasoning as trapping on integer overflow: a program that runs off its own array is wrong, and reading whatever is next in memory is the least useful possible response.

pub fn set(self, index: Int, value: A) ->()

Writes value at index, in place.

The one mutation in core, and the reason Array is not Share: two fibers writing one array is a data race, so an array crosses into a fiber only inside a type that says it is safe.

An index outside the array stops the program, as get does.

pub fn from_fn(count: Int, make: (Int) -> A) -> Array<A>

count elements, each one built by calling make with its index.

The answer when the cells must not be the same object. [Array::new] fills every cell with one value, which is right for a number and wrong for anything with a mut field – an array of accumulators built that way is one accumulator with count names, and it reports every total multiplied by the number of buckets with nothing to say so.

make is called exactly count times, in order, so it may be the source of the difference rather than only of the shape:

let slots: Array<Agg> = Array::from_fn(16, fn _i => { count: 0 });
let squares: Array<Int> = Array::from_fn(10, fn i => i * i);

A count below one is [Array::empty], and make is not called at all – which is the reason this is not Array::new(count, make(0)) with a loop after it.

pub fn sort_by(self, order: (A, A) -> Ordering) ->()

Sorts the whole array in place, stably, by order.

pub fn is_utf8(self) -> Bool

Whether these bytes are well-formed UTF-8, and so whether String::from_bytes will accept them. Only an Array<U8> has an answer.

pub fn prefix(self, count: Int) -> Array<A>

The first count elements, copied.

What a caller does with a buffer it only partly filled: a socket read says how many bytes arrived, and the rest of the buffer is not part of the message.

pub fn with_data<B, 'ef, 'er>(self, body: (Ptr, Int) -> B with 'ef raises 'er) -> B with 'ef raises 'er

Lends the elements to body as a pointer and a count, for the duration of the call and no longer.

This is how a buffer reaches a foreign function. The count is the number of elements — the same number length gives — so a body that wants bytes multiplies by the element’s width itself.

The lifetime is the call, and that is the design rather than a limitation. A plain data(self) -> Ptr would be a dangling pointer waiting to happen: the array is released at its last use, and that use would be the data call. No wider scope is right either — the innermost one is wrong for a pointer taken in one branch of an if, and the function’s own is wrong for a loop, which would hold one buffer alive per iteration.

Only an array of numbers can be lent. An Array<A> of Khora objects holds reference-counted pointers, and handing those across is the mistake the whole boundary exists to prevent.

impl Int

Arithmetic that does not trap, and the bits underneath it.

Ordinary +, - and * stop the program when they do not fit: a program that passes its tests and then wraps in production is the failure worth a branch to prevent. These are how you ask for the other behaviour in the places that genuinely want it — a hash, a checksum, a pseudo-random number — by name, so that the trap stays the default without being in the way.

Methods rather than operators for now. ^, &, |, << and >> are five new tokens and >> has to be told apart from the end of two nested type arguments; none of that is hard and none of it is what a hash was waiting for.

pub fn wrapping_add(self, other: Int) -> Int

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: Int) -> Int

The difference, wrapping.

pub fn wrapping_mul(self, other: Int) -> Int

The product, wrapping.

pub fn xor(self, other: Int) -> Int

Bit by bit: one wherever the two differ.

pub fn and(self, other: Int) -> Int

Bit by bit: one wherever both are one.

pub fn or(self, other: Int) -> Int

Bit by bit: one wherever either is one.

pub fn shl(self, other: Int) -> Int

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: Int) -> Int

Arithmetic, so a negative number stays negative. A logical shift is what Int cannot express and what an unsigned fixed-width type will.

impl String
pub fn slice(self, from: Int, to: Int) -> String

The bytes from from up to to, clamped to what is there.

One allocation and one copy, which is why it is a compiler intrinsic rather than the six lines it reads like. Written in Khora it went through an Array<U8> and String::from_bytes, so every slice was two allocations, two copies and a walk of the bytes to re-check that they were UTF-8 — 2,915 nanoseconds for eighty bytes. String::split_once is two slices and the request parser is a dozen splits, so this one function was most of what a server spent its time on.

Stops the program if the ends fall inside a character, the same bargain String::byte makes about an index. The check is two byte reads rather than a walk: the input is already UTF-8, so the only way out of it is a cut through a multi-byte character, and that shows up as a continuation byte at one end or the other.

Indices outside the string are clamped rather than refused, because “the rest of it” is what every caller of slice(i, byte_length(s)) means.

pub fn is_char_boundary(self, at: Int) -> Bool

Whether at is where a character starts.

The question [slice] gives no way to ask, and it is the one that matters. slice counts bytes and stops the program when the cut lands inside a character:

khora: this slice cuts a character in half, so it is not a String

Which is right – the alternative is a String that is not UTF-8 – but until there was a way to ask, a program truncating text it did not write was one non-ASCII input from dying. std::json copes by reasoning that every byte it escapes is below 128; an application should not have to.

The end of the string is a boundary, and so is 0. Anything outside is not.

pub fn next_boundary(self, at: Int) -> Int

The first character boundary at or after at, or the string’s length.

For walking to a safe place to cut: String::slice(s, 0, s |> next_boundary(20)) takes about twenty bytes and never lands in the middle of anything.

At or after, so it does not advance. Given an offset that is already a boundary this answers that offset, which means

while at < stop {
at = String::next_boundary(s, at); // never ends
}

is an infinite loop on every string. To step a character at a time, pass at + 1 — or better, ask char_at and add Char::utf8_length, which says what it is doing.

The name is the trap: next reads as after. It is kept because the behaviour is the one a cut wants — rounding an arbitrary offset outward to something safe — and because String::chars_between and String::chars are the two loops most callers actually want, and both are here already.

pub fn previous_boundary(self, at: Int) -> Int

The last character boundary at or before at, or 0.

At or before, like its sibling: on a boundary it answers where it already is. See next_boundary for what that costs a caller who steps with it.

pub fn char_at(self, at: Int) -> Option<Char>

The character beginning at byte at, or None.

None when at is outside the string or is not a boundary – asking for the character in the middle of one has no answer, and inventing the replacement character would hide a caller that is stepping by the wrong amount.

Byte offsets, not character indices. char_at(s, 3) is the character three bytes in, and utf8_length is how far to step for the next one. A character index would mean walking the string from the start for every access, which is the quadratic loop this signature refuses to hide.

pub fn chars(self) -> List<Char>

Every character, in order.

Allocates a list as long as the string has characters, which is what makes it the convenient answer rather than the fast one. A scan that cares walks with [char_at] and [Char::utf8_length] and allocates nothing.

pub fn char_length(self) -> Int

How many characters, which is not how many bytes.

Walks the string, so it is O(n). byte_length is the constant-time one and is what a buffer size wants.

pub fn chars_between(self, start: Int, end: Int) -> Int

How many characters lie between two byte offsets.

What a caret needs. A tool reporting on text somebody else wrote places the caret by counting the characters before the span and draws it by counting the characters inside it, and char_length counts a whole string while byte_length counts the wrong unit. examples/khq wrote this for itself before it was here.

start is rounded down to a boundary and end up, so a range that splits a character counts it once rather than trapping. A caller who wants to know whether it split one has is_char_boundary.

pub fn find(self, needle: String, from: Int) -> Int

Where needle first occurs at or after from, counting in bytes, or -1 if it is nowhere.

The one place in this library that answers with a sentinel, and it earns it the way String::byte earns being able to stop the program: this is the bottom of the string library, index_of is one line above it and is what everything else calls. A search is the C library’s memmem, which says -1, and turning that into an Option is work the compiler cannot do inside an intrinsic — so it is done here, once, in the open.

from exists so that scanning a string does not mean copying it. Walking a header block by slicing off the front costs a copy of everything left over, once per header; searching from a cursor copies only the pieces that are kept.

An empty needle is found at from, which is what every other language says and what stops split_once on an empty separator from running forever.

pub fn index_of(self, needle: String) -> Option<Int>

Where needle first occurs, or nothing.

A compiler intrinsic underneath, and the reason is a measurement. Written as a loop over String::byte this took 3,180 nanoseconds to find a six-byte word in eighty bytes — a call and a bounds check per byte, at every candidate position. Bound to memmem but reaching it through String::with_data it still took 500, because the two nested closures that borrow the bytes are two heap allocations. Going straight to the runtime call is 40.

That matters because this is the hottest thing in the request parser: split_once is built on it, and parsing one request is about eight searches.

pub fn matches_at(self, needle: String, at: Int) -> Bool

Whether needle sits at exactly at, without cutting a string out to ask.

The primitive find, starts_with and ends_with are all written over: comparing in place costs nothing, and slice would allocate once per position tried.

pub fn starts_with(self, prefix: String) -> Bool

Whether the text begins with prefix. A prefix longer than the text is simply false rather than an index that would go past the end.

pub fn ends_with(self, suffix: String) -> Bool

Whether the text ends with suffix.

pub fn contains(self, needle: String) -> Bool

Whether needle occurs anywhere.

The empty string is in everything, which is what find already answers and is the convention every library that has thought about it settled on: it is the identity of concatenation, and it is at position 0 of anything.

pub fn is_empty(self) -> Bool

Whether there are no bytes at all.

pub fn trim(self) -> String

The string without leading or trailing whitespace.

Space, tab, carriage return and newline – the four a line of text picks up on its way through a file or a socket. Not Unicode’s full set of space characters: that list changes with the standard, and a std that silently trimmed a non-breaking space would be surprising in the other direction.

The ends move inwards over the original bytes and only what survives is copied, so trimming nothing costs one slice and no allocation.

pub fn lower(self) -> String

The string with A-Z written in lower case.

ASCII only, and deliberately. Unicode case folding is locale dependent – Turkish I lowercases to a dotless ı, and German ß upper-cases to two characters – so a correct lower needs a locale and a table, and neither belongs in core. What this is for is comparing a header name or a file extension, where the input is ASCII by specification.

Bytes outside A-Z are untouched, so text in any encoding passes through rather than being mangled.

pub fn upper(self) -> String

The same text with a-z shifted to A-Z.

ASCII only, for the reason lower gives: a case mapping that is right for every script is a table that changes with the Unicode standard, and one that is right for some scripts silently is worse than one that is honest about being ASCII. Turkish dotless i is the example everybody learns this from.

pub fn trim_start(self) -> String

The text with leading whitespace removed.

pub fn trim_end(self) -> String

The text with trailing whitespace removed.

pub fn split(self, separator: String) -> List<String>

Every piece between occurrences of separator, in order.

n separators give n + 1 pieces, always, which is the rule that makes splitting and joining inverses: "" splits to one empty piece, "a," splits to two of which the second is empty, and join(split(s, x), x) is s for every s. Dropping empty pieces is a filter the caller can write and cannot undo.

An empty separator would match everywhere and split nothing, so it gives back the whole string as one piece rather than looping.

pub fn join(pieces: List<String>, separator: String) -> String

The pieces laid end to end with separator between them.

The inverse of split for the same separator, which is the property that decides both. Written here rather than on List because it is about text and not about lists: a List<Int> has no join.

Adjacent pairs, halving, rather than one piece at a time. Written the obvious way — head + separator + join(tail) — this was wrong twice. It recursed once per piece, so it stopped the program somewhere past four thousand of them; and every one of those concatenations copied the whole answer so far, so the work was quadratic in the output. Measured: joining a thousand ten-byte pieces took under a millisecond, two thousand took one, four thousand took four.

Combining adjacent pairs copies the whole length once per round, and there are log2(count) rounds. Sixteen thousand pieces goes from a gigabyte and a half of copying to about two megabytes, using the same +, and the walk is a loop rather than a stack.

pub fn replace(self, from: String, to: String) -> String

Every occurrence of from replaced by to.

Left to right over the original, so what to puts in is never searched again: replacing "a" with "aa" doubles each a once rather than looping. An empty from matches everywhere and changes nothing, which is the only answer that terminates.

pub fn repeat(self, count: Int) -> String

The text count times over. Below one is empty.

By doubling, for the reason join above pairs. self + repeat(self, count - 1) was a stack frame per repetition and copied the whole of what it had built at every one of them, so asking for a line of ten thousand dashes did fifty million bytes of work and asking for a hundred thousand stopped the program. Squaring the chunk and taking the bits of count makes it a handful of copies of the answer’s own length.

pub fn is_space(byte: U8) -> Bool

Whether a byte is one of the four whitespace characters trim removes.

pub fn split_once(self, separator: String) -> Option<Split>

The text either side of the first separator, or nothing if it does not appear.

For a header line, a key=value pair, a URL and its query — anywhere the separator may legitimately appear again in the second half and splitting on every one of them would be wrong.

One split rather than a list of them, because a list wants an allocation per piece and every caller here wants the head and the rest. A record rather than a pair, because .head says which one it is and .0 does not.

pub fn pad_left(self, width: Int, fill: String) -> String

self with fill in front of it until it is width bytes long.

For a column, which is the only reason anybody wants it. Every one of the three programs written against this library grew its own, and std::time has a third copy under the name padded – a function four people write is a function the library should have.

Bytes rather than characters, which matters the moment the text is not ASCII: a column of names in Greek will not line up, and lining up text by display width is a font question this type cannot answer. byte_length is the length everything else here counts in, so this counts in it too.

Already at or past width is returned unchanged: a truncating pad is a different function, and one that silently loses a digit from a total is not one this library is going to offer by accident. A fill that is not one byte long is refused the same way, since the loop could not land on width exactly.

pub fn pad_right(self, width: Int, fill: String) -> String

self with fill after it until it is width bytes long.

The other side of pad_left, and the one a left-aligned label wants.

pub fn split_whitespace(self) -> List<String>

The pieces between runs of whitespace, with no empty ones.

Not split(" "), and the difference is the whole point: split keeps every empty piece, which is right for a CSV – where two commas in a row mean a field that is there and blank – and wrong for anything column-aligned, where two spaces in a row mean the writer was lining something up. A log line split the first way has empty strings in it that no reader of the log put there.

Whitespace is what is_space says: space, tab, carriage return and newline.

impl Float
pub fn to_string(self) -> String

The number, written out — 1.5, 0.1, inf.

The shortest form that reads back as the same number, which is what a reader means by “the number” and what print on a Float already gives. Thirty-two bytes is always enough for a double; the call says how many it needed, so a longer answer would be caught rather than truncated.

pub fn to_fixed(self, places: Int) -> String

The number to exactly places decimal places.

For a column and for a percentage, which is what everybody wanted it for: to_string gives the shortest form that reads back, so 0.5 and 0.125 are three characters apart and a table of them does not line up. Two of the review programs wrote the same percent(part, whole) helper and examples/risk_analyzer had a third copy, and the rounding was the half none of them wanted to write.

Not written in Khora, and the reason is the point of the whole numeric design. Multiplying by 10^places, rounding and dividing back does the rounding in binary floating point — so a percentage renders as 33.33 on one machine and 33.34 on another, which is the bug std::decimal exists to prevent, reintroduced by the formatter. The runtime rounds the decimal expansion of the double instead, the way C’s printf and Go’s strconv do, so this and its neighbours agree.

Still a Float, so it is still a rounding of an approximation. 0.1 + 0.2 to two places is 0.30, and it is 0.30 because the sum was near enough, not because it was right. Money is Decimal and Decimal::rounded is the exact version of this; the type that cannot have an Eq cannot have an exact one of these either.

places is clamped to nought through thirty: a double carries about seventeen significant digits, so past that the characters are an artefact of the binary value rather than information.

pub fn to_int(self) -> Int

The whole part, with nothing rounded.

Truncates toward zero — 2.9 is 2 and -2.9 is -2 — which is what every machine’s “convert to integer” does, and what C, Rust and Go all mean by it. Rounding is a different question with four defensible answers, so it gets a different function when somebody needs one.

A number too large for an Int clamps to the nearest end, and a NaN is zero. Undefined behaviour is the alternative and is not one.

pub fn of_string(text: String) -> Option<Float>

The number text names, or None if it does not name one.

The shape is JSON’s, which is also what a person writes: an optional -, digits, an optional . and digits, an optional e or E with an optional sign and digits. No + in front, no underscores, no spaces, no hexadecimal, no inf and no nan — a parser that accepts more than the caller expected is how a configuration value ends up meaning something nobody wrote, and the two names are worse than that because they are values arithmetic cannot get back out of.

An Option for Int::of_string’s reason: the text came from outside, and somebody typing it wrong is a thing to answer rather than to stop the program over.

This existed already, privately, in std::json. Its comment said so: “This is Float::of_string in all but name and would be better as one in std::core. It is here because std::json is where the need is and a number parser in core is a decision worth making on its own evidence.” The evidence arrived as examples/khq, which has to read the numbers in a query and could not, and which found in the same hour that Float is the only primitive with no way in from text — Int and Decimal both have one.

Inexact by construction, and that is the type rather than this function: 0.1 is not a binary fraction and never will be. Decimal::of_string is the one to reach for when the digits have to survive.

impl Int
pub fn of_string(text: String) -> Option<Int>

The number a string spells, or nothing.

An Option rather than a trap, because the text almost always came from outside — an environment variable, a header, a path segment, a form field — and “somebody typed it wrong” is a thing to answer with a 400 rather than to stop the program over.

Leading - and nothing else: no +, no underscores, no spaces, no hexadecimal. A parser that accepts more than the caller expected is how a configuration value ends up meaning something nobody wrote.

Refuses rather than overflows. * and + trap in this language, so a long enough run of digits would have stopped the program — which for text off a socket is a denial of service with extra steps. The running total is checked before it is grown instead.

The guard is exact rather than approximate: it asks for the room one more digit needs, so "9223372036854775808" — one past the largest Int, and the shape of a number somebody fat-fingered into a form — is refused rather than wrapped or fatal.

The total is counted downward, which is why the guard is written against smallest_int rather than a largest one. Every Int has a negative twin and not every one has a positive twin, so building 9223372036854775808 on the way to -9223372036854775808 would make the most negative number the one value this could not read.

pub fn to_float(self) -> Float

The same number as a Float.

Exact up to 2^53 and rounded beyond it, which is IEEE’s business rather than Khora’s.

pub fn to_string(self) -> String

The number, written out.

Needed by anything that puts a number in a message — a Content-Length, a status line, a log — and written here rather than in the runtime because it is four lines of Khora and a good test of whether they are pleasant to write.

The most negative Int prints. Taking its magnitude with 0 - self overflows — that is the whole peculiarity of the number — so the digits are never reached that way.

The digits are walked on whichever side of zero the number is already on, so nothing is ever negated.

impl Ptr
pub fn null() -> Ptr

The null pointer, for the libraries that take one.

pub fn is_null(self) -> Bool

Whether this is null — which is how a great many C functions say they failed.

impl String

A string’s bytes.

Length and index are both in bytes, and say so in their names. A String is UTF-8, so a character is one to four of these, and a length that quietly meant one of the two would be wrong for half its callers and silent about which half. Anything that wants characters wants a decoder, which is a library on top of this rather than a different meaning for the same word.

pub fn byte_length(self) -> Int

How many bytes the string occupies. Not how many characters it has.

pub fn byte(self, index: Int) -> U8

The byte at index, counting from zero. Out of range stops the program, the same as an array.

pub fn from_bytes(bytes: Array<U8>) -> String

The same bytes, as a String.

Stops the program if they are not UTF-8. The same bargain get makes about an index: the check exists — Array::is_utf8 — and calling this without it is the mistake. An Option was the alternative and would put the decision in the wrong place, because what to do about bytes that are not text depends entirely on where they came from.

pub fn bytes(self) -> Array<U8>

A copy of the bytes.

A copy rather than a view, because a string is immutable and an array is not — a view would let one be edited through the other.

pub fn with_c_string<B, 'ef, 'er>(self, body: (Ptr) -> B with 'ef raises 'er) -> B with 'ef raises 'er

Lends the bytes to body as a pointer and a length, for the duration of the call and no longer.

Lends the bytes to body as a const char *: the same bytes with a zero after them, which is how every function in the C library finds the end of a string.

A copy, necessarily — a Khora string knows its length instead of carrying a terminator, and there is nowhere to append one to a borrowed view. The copy lives exactly as long as the call.

A string with a zero inside it is not refused: C will see a shorter string than Khora has, which is what C strings are.

pub fn with_data<B, 'ef, 'er>(self, body: (Ptr, Int) -> B with 'ef raises 'er) -> B with 'ef raises 'er

See Array::with_data, which is the same thing and carries the argument for why the lifetime is a call rather than anything wider.

impl U8

An unsigned byte: 0 to 255.

The one every other type here is in service of — a wire format, a file and a string are all made of these.

pub fn of(value: Int) -> U8

The same number as a U8, or a stopped program if it does not fit.

Explicit, like every conversion in Khora: there is no implicit narrowing anywhere, because a number that silently becomes a different number is found in production rather than in a test.

pub fn wrapping(value: Int) -> U8

The low bits of value, whatever they are. The way to ask for truncation by name, for the wire formats that mean it.

pub fn to_int(self) -> Int

Every U8 is an Int, so this cannot fail and costs one instruction.

pub fn wrapping_add(self, other: U8) -> U8

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: U8) -> U8

The difference, wrapping.

pub fn wrapping_mul(self, other: U8) -> U8

The product, wrapping.

pub fn xor(self, other: U8) -> U8

Bit by bit: one wherever the two differ.

pub fn and(self, other: U8) -> U8

Bit by bit: one wherever both are one.

pub fn or(self, other: U8) -> U8

Bit by bit: one wherever either is one.

pub fn shl(self, other: U8) -> U8

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: U8) -> U8

Logical, because the type is unsigned: the vacated bits are zeros and a large value stays large.

impl U16

An unsigned 16-bit integer: 0 to 65,535.

pub fn of(value: Int) -> U16

The same number as a U16, or a stopped program if it does not fit.

Explicit, like every conversion in Khora: there is no implicit narrowing anywhere, because a number that silently becomes a different number is found in production rather than in a test.

pub fn wrapping(value: Int) -> U16

The low bits of value, whatever they are. The way to ask for truncation by name, for the wire formats that mean it.

pub fn to_int(self) -> Int

Every U16 is an Int, so this cannot fail and costs one instruction.

pub fn wrapping_add(self, other: U16) -> U16

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: U16) -> U16

The difference, wrapping.

pub fn wrapping_mul(self, other: U16) -> U16

The product, wrapping.

pub fn xor(self, other: U16) -> U16

Bit by bit: one wherever the two differ.

pub fn and(self, other: U16) -> U16

Bit by bit: one wherever both are one.

pub fn or(self, other: U16) -> U16

Bit by bit: one wherever either is one.

pub fn shl(self, other: U16) -> U16

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: U16) -> U16

Logical, because the type is unsigned: the vacated bits are zeros and a large value stays large.

impl U32

An unsigned 32-bit integer: 0 to 4,294,967,295.

pub fn of(value: Int) -> U32

The same number as a U32, or a stopped program if it does not fit.

Explicit, like every conversion in Khora: there is no implicit narrowing anywhere, because a number that silently becomes a different number is found in production rather than in a test.

pub fn wrapping(value: Int) -> U32

The low bits of value, whatever they are. The way to ask for truncation by name, for the wire formats that mean it.

pub fn to_int(self) -> Int

Every U32 is an Int, so this cannot fail and costs one instruction.

pub fn wrapping_add(self, other: U32) -> U32

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: U32) -> U32

The difference, wrapping.

pub fn wrapping_mul(self, other: U32) -> U32

The product, wrapping.

pub fn xor(self, other: U32) -> U32

Bit by bit: one wherever the two differ.

pub fn and(self, other: U32) -> U32

Bit by bit: one wherever both are one.

pub fn or(self, other: U32) -> U32

Bit by bit: one wherever either is one.

pub fn shl(self, other: U32) -> U32

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: U32) -> U32

Logical, because the type is unsigned: the vacated bits are zeros and a large value stays large.

impl U64

An unsigned 64-bit integer: 0 to 18,446,744,073,709,551,615.

The only type here that holds numbers Int cannot, which is why it is the only one whose to_int can stop the program.

pub fn of(value: Int) -> U64

The same number as a U64, or a stopped program if it does not fit.

Explicit, like every conversion in Khora: there is no implicit narrowing anywhere, because a number that silently becomes a different number is found in production rather than in a test.

pub fn wrapping(value: Int) -> U64

The low bits of value, whatever they are. The way to ask for truncation by name, for the wire formats that mean it.

pub fn to_int(self) -> Int

Int cannot hold every U64, so this stops the program above Int’s maximum. U64::wrapping_to_int is how to reinterpret the bits instead.

pub fn wrapping_to_int(self) -> Int

The same bits read as a signed Int, so a U64 above Int’s maximum comes back negative rather than stopping.

pub fn wrapping_add(self, other: U64) -> U64

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: U64) -> U64

The difference, wrapping.

pub fn wrapping_mul(self, other: U64) -> U64

The product, wrapping.

pub fn xor(self, other: U64) -> U64

Bit by bit: one wherever the two differ.

pub fn and(self, other: U64) -> U64

Bit by bit: one wherever both are one.

pub fn or(self, other: U64) -> U64

Bit by bit: one wherever either is one.

pub fn shl(self, other: U64) -> U64

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: U64) -> U64

Logical, because the type is unsigned: the vacated bits are zeros and a large value stays large.

impl I8

A signed 8-bit integer: -128 to 127.

pub fn of(value: Int) -> I8

The same number as a I8, or a stopped program if it does not fit.

Explicit, like every conversion in Khora: there is no implicit narrowing anywhere, because a number that silently becomes a different number is found in production rather than in a test.

pub fn wrapping(value: Int) -> I8

The low bits of value, whatever they are. The way to ask for truncation by name, for the wire formats that mean it.

pub fn to_int(self) -> Int

Every I8 is an Int, so this cannot fail and costs one instruction.

pub fn wrapping_add(self, other: I8) -> I8

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: I8) -> I8

The difference, wrapping.

pub fn wrapping_mul(self, other: I8) -> I8

The product, wrapping.

pub fn xor(self, other: I8) -> I8

Bit by bit: one wherever the two differ.

pub fn and(self, other: I8) -> I8

Bit by bit: one wherever both are one.

pub fn or(self, other: I8) -> I8

Bit by bit: one wherever either is one.

pub fn shl(self, other: I8) -> I8

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: I8) -> I8

Arithmetic, because the type is signed: a negative number stays negative.

impl I16

A signed 16-bit integer: -32,768 to 32,767.

pub fn of(value: Int) -> I16

The same number as a I16, or a stopped program if it does not fit.

Explicit, like every conversion in Khora: there is no implicit narrowing anywhere, because a number that silently becomes a different number is found in production rather than in a test.

pub fn wrapping(value: Int) -> I16

The low bits of value, whatever they are. The way to ask for truncation by name, for the wire formats that mean it.

pub fn to_int(self) -> Int

Every I16 is an Int, so this cannot fail and costs one instruction.

pub fn wrapping_add(self, other: I16) -> I16

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: I16) -> I16

The difference, wrapping.

pub fn wrapping_mul(self, other: I16) -> I16

The product, wrapping.

pub fn xor(self, other: I16) -> I16

Bit by bit: one wherever the two differ.

pub fn and(self, other: I16) -> I16

Bit by bit: one wherever both are one.

pub fn or(self, other: I16) -> I16

Bit by bit: one wherever either is one.

pub fn shl(self, other: I16) -> I16

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: I16) -> I16

Arithmetic, because the type is signed: a negative number stays negative.

impl I32

A signed 32-bit integer: -2,147,483,648 to 2,147,483,647.

A C int, which is why it is the one a foreign function usually wants.

pub fn of(value: Int) -> I32

The same number as a I32, or a stopped program if it does not fit.

Explicit, like every conversion in Khora: there is no implicit narrowing anywhere, because a number that silently becomes a different number is found in production rather than in a test.

pub fn wrapping(value: Int) -> I32

The low bits of value, whatever they are. The way to ask for truncation by name, for the wire formats that mean it.

pub fn to_int(self) -> Int

Every I32 is an Int, so this cannot fail and costs one instruction.

pub fn wrapping_add(self, other: I32) -> I32

The sum, wrapping at the type’s own range instead of trapping.

pub fn wrapping_sub(self, other: I32) -> I32

The difference, wrapping.

pub fn wrapping_mul(self, other: I32) -> I32

The product, wrapping.

pub fn xor(self, other: I32) -> I32

Bit by bit: one wherever the two differ.

pub fn and(self, other: I32) -> I32

Bit by bit: one wherever both are one.

pub fn or(self, other: I32) -> I32

Bit by bit: one wherever either is one.

pub fn shl(self, other: I32) -> I32

Towards the high bits, with zeros coming in at the bottom and whatever leaves the top gone.

pub fn shr(self, other: I32) -> I32

Arithmetic, because the type is signed: a negative number stays negative.

impl<K, V> Dict<K, V>
pub fn new() -> Dict<K, V>

An empty dictionary.

pub fn size(self) -> Int

How many entries. Kept in the node rather than counted, because balancing asks for it at every step.

pub fn is_empty(self) -> Bool

Whether there are no entries.

pub fn rotate_left(key: K, value: V, left: Dict<K, V>, right: Dict<K, V>) -> Dict<K, V>

The right side is heavy. Which rotation depends on its own shape: a single one moves its left subtree across, which only helps if that subtree is the lighter half.

pub fn rotate_right(key: K, value: V, left: Dict<K, V>, right: Dict<K, V>) -> Dict<K, V>

The mirror of it.

pub fn entries(self) -> List<Pair<K, V>>

Every entry, smallest key first.

pub fn keys(self) -> List<K>

Every key, in order.

Here because Map has keys and values and this had only entries, which left a reader who found one and not the other to work out whether the difference meant something. It did not.

pub fn values(self) -> List<V>

Every value, in key order.

impl<K: Ord, V> Dict<K, V>
pub fn get(self, key: K) -> Option<V>

The value under key, if there is one.

pub fn contains(self, key: K) -> Bool

Whether key has an entry.

pub fn insert(self, key: K, value: V) -> Dict<K, V>

The map with key set to value, leaving this one as it was.

pub fn update(self, key: K, step: (Option<V>) -> V) -> Dict<K, V>

The map with key set to whatever step makes of what was there.

Counting things into a map is the most common thing a log analyser does, and without this it is insert(t, k, unwrap_or(get(t, k), 0) + 1) every time – which names the map three times, the key twice, and walks the tree twice to answer one question.

step is given None when the key is new, so the initial value and the update are the same expression:

counts = Dict::update(counts, word, fn seen => seen.unwrap_or(0) + 1);

One walk, and step is called exactly once. There is no version that removes the entry: step returns a V rather than an Option<V> because “update or delete” is two operations wearing one name, and remove is right there.

pub fn remove(self, key: K) -> Dict<K, V>

The map without key. Removing what is not there is the map unchanged.

pub fn glue(left: Dict<K, V>, right: Dict<K, V>) -> Dict<K, V>

Joins two sides whose node has gone.

The successor moves up: every key on the left is smaller than every key on the right, so the smallest of the right is the one that can sit between them.

pub fn from_list(entries: List<Pair<K, V>>) -> Dict<K, V>

Builds a map from entries, later ones winning.

A loop rather than a fold, because the recursion would be as deep as the list is long — the cliff String::slice fell off.

impl<K: Hash, V> Map<K, V>
pub fn new() -> Map<K, V>

An empty map, which allocates nothing but itself.

The buckets wait for the first insert, so a map that is never written to costs nothing beyond the map itself. A great many are never written to: a request with no query string still builds a query map, and a route with no :name still builds one for its parameters.

pub fn len(self) -> Int

How many entries there are. Counted as it goes, not walked.

pub fn get(self, key: K) -> Option<V>

The value under key, if there is one.

pub fn holds(self, key: K) -> Bool

Whether key has an entry.

For when the value is not wanted; get is one lookup either way, so holds followed by get is two and a match on get is one.

pub fn insert(self, key: K, value: V) ->()

Adds key, replacing whatever was there. The count only moves when the key is new, which is what makes it the number of entries rather than the number of insertions.

pub fn update(self, key: K, step: (Option<V>) -> V) ->()

Puts step of whatever is under key back under it.

One hash instead of two, which is the whole point. Counting things is the commonest thing a mutable map is for, and written with what was here it is

let seen = match Map::get(counts, word) {
Option::Some(n) => n + 1,
Option::None => 1,
};
Map::insert(counts, word, seen);

— two hashes of the same key, two walks of the same bucket and an Option built in between, on the hot line of every histogram, word count and group-by anybody writes. This is

Map::update(counts, word, fn seen => match seen {
Option::Some(n) => n + 1,
Option::None => 1,
});

step is handed None when the key is new, so it is also how a default is written without saying the key twice. The mirror of [Dict::update], which answers a new dictionary where this changes the one it is given.

pub fn remove(self, key: K) ->()

Takes key out, in place. Removing what is not there does nothing.

The table never shrinks. A map emptied of a million entries keeps its million buckets, which nothing here has needed to be otherwise.

pub fn entries(self) -> List<Pair<K, V>>

Every entry, as a list.

In no particular order. A hash table forgets the order its keys arrived in, and the order this does report is not even stable across inserts: growing rehashes everything into twice as many buckets and moves roughly half of it. A caller that wants an order sorts what comes back, or uses Dict, whose entries is in key order because the tree already is.

Without this, iterating a map means reaching into buckets and matching on Chain, which is what std::json had to do to write an object back out — a module coupled to the table’s representation for want of six lines here.

Two loops rather than two recursions. A table with a million entries has a million buckets to visit, and a stack frame each is the mistake String::slice made per byte. The inner walk is a loop too, for a less obvious reason: a chain is only short while the Hash is good, and a bad one puts every entry in a single bucket. The price of that should be a slow walk, not a smashed stack.

pub fn keys(self) -> List<K>

The keys, in the reverse of the order entries reports them — which is to say in no particular order either.

Built by walking entries rather than the buckets a second time. The pairs it allocates and throws away are the price of having one place where the traversal is written; a map big enough for that to matter wants entries itself, which allocates the pairs the caller was going to keep.

Not List::map, which recurses once per element and would put a ceiling on the size of map this can be called on.

keys and values walk the same list the same way, so the two line up position for position. That is the only thing worth promising about the order of either.

pub fn values(self) -> List<V>

The values, one per entry — duplicates included, because two keys may well hold the same value and dropping one would make values disagree with len.

impl<A> Vector<A>
pub fn new() -> Vector<A>

Empty, holding no storage at all.

Eight is what the first push will ask for, matching Map::new’s eight buckets — enough that a small vector never reallocates, small enough that a vector which is made and never pushed to, as a field on a record or an accumulator on the branch that turned out to have nothing to accumulate, has cost two objects and no cells: itself, and the empty array’s header.

pub fn with_capacity(capacity: Int) -> Vector<A>

Room for capacity elements, from the first push onwards.

Nothing is allocated here — there is no value of A to fill an array with yet — so this records the size and the first push allocates it. The saving is real either way: what with_capacity buys is that filling it reallocates once instead of a dozen times, and that is a property of the first allocation’s size, not of when it happens.

A capacity that is not positive is no opinion at all, and gets the same eight new would have. Array::new would trap on a negative length, and rightly, but a capacity is a hint about a size that is often computed — expected - already_have — and being wrong about a hint is not the same kind of mistake as indexing off the end of an array.

pub fn length(self) -> Int

How many elements are in it.

pub fn is_empty(self) -> Bool

Whether there is nothing in it.

pub fn capacity(self) -> Int

How many elements fit before the next reallocation — allocated cells, not promised ones, so this is nought until the first push.

Worth exposing because growth is the only thing about a vector that is not visible from the outside, and a test that means to exercise it has to be able to see it.

pub fn push(self, item: A) ->()

Adds an element at the end.

The element goes to grow as well when the array is full, because it is the fill: the only value of A anyone here has.

pub fn grow(self, seed: A) ->()

Twice the room, or wanted if there was none, with seed in every cell.

Doubling, not a fixed increment, which is the whole reason push is amortised constant: growing by a constant makes filling a vector quadratic in its length, and that is the bug that turns a working program into a slow one only once the input gets big. Two rather than the 1.5 some allocators are tuned for, because reusing the freed blocks needs an allocator that coalesces them and this runtime hands them straight to malloc; the smaller factor would buy nothing and copy more often.

The cells past len hold seed — a copy of the element being pushed, because there is nothing else of type A to put there. They are overwritten by the pushes that follow, and until then a boxed A is held by that many references rather than that many objects.

The one visible consequence: a vector that grew and was then popped back down keeps that element alive, because the cells above the length were never written over. It is a single object however long the vector is — all of those cells hold the same one — and it goes at the next growth or with the array. Blanking them would need a value of A to blank them with, which is the same wall this whole type is built around.

pub fn pop(self) -> Option<A>

Removes the last element and hands it back.

Option::None from an empty vector rather than a trap: draining one is match Vector::pop(v) until it answers None, which makes empty a loop condition rather than a mistake. That is the opposite call from Array::get, and deliberately — an index is a claim about a length the caller believes it knows, while popping is a question.

The cell that is left over has to hold something of type A, so it is given the first element. That is not tidiness: without it the array would go on holding the element that was just removed, and a vector popped down and then kept would be indistinguishable from a leak. Popping the last element has no first element to use and needs none — the array goes, all at once, and wanted keeps its size for the next fill.

pub fn get(self, index: Int) -> Option<A>

The element at index, or Option::None if there is none there.

Soft where Array::get traps, and not only for symmetry with pop: an array’s length is fixed and known where it was allocated, while a vector’s is whatever the pushes left it at, so a caller reading one is far more often asking than asserting.

Bounded by len and never by Array::length. The cells in between are not empty — they hold the fill, or an element a pop left behind — and handing one of those back would be a phantom element rather than an error.

pub fn at(self, index: Int) -> A

The element at index, or the program stops.

The one that does not allocate. [Vector::get] answers an Option<A>, which is right when the index is a question — and it is wrong when it is not. Walking a vector by index is the ordinary way to read one, and every read of that walk allocated a Some for the caller to match away one line later. The bound is checked either way; what this removes is the object built to carry an answer that was never in doubt.

So the two are a pair, and which one to reach for is a question about the index rather than about speed: get when the index came from outside and might not be there, at when the loop that produced it is right above. Array::get has been the second of those all along, and a vector had only the first.

Out of range stops the program and says which index and how long the vector was — bounded by len and not by the array underneath it, so the cells a pop left behind are past the end here as they are everywhere else.

pub fn sort_by(self, order: (A, A) -> Ordering) ->()

Sorts the vector in place, stably, by order.

The elements, and not the array underneath them: a vector’s array is as long as the last push made it and the cells past len hold whatever a pop left behind, so sorting the array would sort those into the middle of the answer.

pub fn set(self, index: Int, value: A) -> Bool

Replaces the element at index. Answers whether there was one to replace.

A write past the end is not a question with an answer — there is no element there — so this reports rather than inventing one or growing to fit. Growing would have to fill the gap between, and the gap is exactly what there is no value of A for; push is how a vector gets longer.

pub fn clear(self) ->()

Empties it.

The array goes rather than being kept and blanked, because there is nothing to blank a cell with: keeping it would keep every element in it alive until something was pushed over that cell, and a cleared vector holding its contents is the bug this method is usually called to avoid. wanted remembers how big it was, so the next fill reallocates once and arrives back where it started.

pub fn to_list(self) -> List<A>

The elements, first to last.

Backwards through the array, prepending, so the list comes out in order without a reversing pass — the same trick Map::entries uses, and a loop for the same reason: one frame per element is what String::slice overflowed the stack with.

pub fn from_list(items: List<A>) -> Vector<A>

A vector of the list’s elements, in the same order.

Pushed one at a time into an empty vector rather than sized up front with List::length, on the argument that pre-sizing costs a second full walk of the list to save a handful of reallocations that double — and a walk of the whole input is the more expensive half of that trade.

impl<A, B, 'er> SharedFn<A, B, 'er>
pub fn of(f: (A) -> B raises 'er) -> SharedFn<A, B, 'er>

Certifies a closure written here.

The argument has to be a closure literal or a named function — anything else was written elsewhere and took its captures with it, which is the one thing this cannot check and so the one thing it refuses.

pub fn call(self, argument: A) -> B raises 'er

Calls it. A plain closure call; the wrapper costs nothing at runtime.

impl<A: Share> Channel<A>
pub fn bounded(capacity: Int) -> Channel<A>

A channel that will hold at most capacity values.

Below one is one — see the type’s note on rendezvous.

A full one makes the sender wait, which is backpressure and is the right default: a queue nobody is draining is a producer that should slow down. dropping and sliding are the two ways to say the opposite.

pub fn dropping(capacity: Int) -> Channel<A>

Like bounded, but a send into a full one fails instead of waiting.

For a queue whose producer must not stall: the request path writing an audit event, a handler emitting a metric. send answers false, so the loss is a value the caller can count and report — which is the whole difference from sliding, where it is not.

The choice is the channel’s rather than the send’s, because a queue is lossy or it is not, and two senders disagreeing about which is not a state a queue can be in.

pub fn sliding(capacity: Int) -> Channel<A>

Like bounded, but a send into a full one evicts the oldest value.

For a feed where the newest value is the only one worth having: a gauge, a progress indicator, the last-known position of something. send answers true — nothing was refused — so the loss is invisible at the call site, and that is deliberate: nobody was going to act on it. Reach for dropping where somebody would.

pub fn send<'er>(self, value: A) -> Bool raises 'er

Puts a value in, waiting while the channel is full.

False when the channel is closed, in which case the value is released rather than kept: a send with nowhere to put its value must not be the quietest possible leak.

A cancellation point, which is what the row is for. Waiting for room is one of the two places in this library where a fiber can sit indefinitely, so Fiber::cancel has to be able to reach it – and a cancellation travels out on a raises row or not at all. The row is a variable because nothing here raises an error of its own; it carries the caller’s. A send that had to give up releases the value, the same as a closed one.

pub fn receive<'er>(self) -> Option<A> raises 'er

Takes a value out, waiting while the channel is empty.

None only when the channel is closed and drained.

A cancellation point, for the reason send gives. A receive that is cancelled is always cancelled empty-handed: the runtime looks at the flag only once it has established the queue is empty, so a send arriving at the same moment still wins and the value is never dropped on the floor.

pub fn poll(self) -> Option<A>

Takes a value if one is already there, and never waits.

None means “not right now”, not “not ever”. A closed and drained channel and a live empty one both answer None, and telling them apart is what receive is for. This exists for a loop with something else to do between looks; a loop that only polls is a loop that spins, and the answer to that is receive.

pub fn close(self) ->()

Says nothing more will be sent, and releases everybody waiting.

pub fn depth(self) -> Int

How many values are waiting to be taken.

Stale the moment it is given, which is true of every such count. For a pool reporting its depth and for tests; nothing should branch on it.

impl<A: Share> Shared<A>
pub fn of(value: A) -> Shared<A>

A cell holding value.

pub fn get(self) -> A

What is in it now, as a value of the caller’s own.

pub fn set(self, value: A) ->()

Replaces what is in it.

pub fn update(self, change: (A) -> A) -> A

Reads, transforms and writes as one step, and gives back the new value.

change runs once, under the lock. It cannot fail, and that is deliberate: a function with no error row has no channel to be cancelled on, so nothing can leave the critical section except by returning and there is no path on which the lock is still held. Work that can fail belongs outside — compute it, then set the answer.

Updating the same cell from inside change is a deadlock, and stops the program with a message rather than waiting for itself.

pub fn modify<B>(self, change: (A) -> Changed<A, B>) -> B

The same, for a change that has something to say beyond the new state.

update can only answer with what it left in the cell, which is not always what the caller needs to know. A handler inserting a record under a generated key has to get that key back, and searching the new state for it afterwards is a guess — a wrong one the moment two fibers insert records that look alike:

let code = Shared::modify(links, fn table => {
let drawn = fresh_code(table);
{ state: Dict::insert(table, drawn, made), result: drawn }
});

Both halves are installed and handed back under the one lock, so the answer belongs to the state it was produced with.

impl Fibers
pub fn open() -> Fibers

A nursery with no limit on how many children it holds at once.

bounded is usually the one to want: a server adopting a fiber per connection meets an unbounded nursery’s ceiling by exhausting memory.

pub fn bounded(limit: Int) -> Fibers

A nursery that will hold at most limit running children.

Adopting past the limit waits for the oldest to finish. That is the whole of it, and it is what turns a capacity ceiling into a queue: a server adopting a fiber per connection meets an unbounded nursery’s ceiling by exhausting memory, and meets a bounded one’s by accepting more slowly, which the listening socket’s backlog is already built to absorb.

Unbounded is right for a nursery over a known handful of tasks — the shape nursery(..) is usually used for — and wrong for one fed by the outside world.

pub fn adopt<'er>(self, fiber: Fiber<(), 'er>) ->()

Puts a running fiber under this nursery.

The nursery takes the handle, so nothing else can outlive it with one. Adopting past a bounded limit waits for the oldest child to finish, which is what turns a ceiling into a queue.

Fiber<(), 'er>: adopting gives up the answer and keeps the failure. A nursery holds its children as bare handles and waits for them; it could not hand back what they computed even if it kept it, so the answer is fixed at (). The row stays, because a cancellation travels out on the same tagged return an error does — a child whose row is empty has no channel to be stopped on, and a nursery that cannot stop its children is not a nursery.

pub fn wait(self) -> Int

Waits for every child, oldest first, and empties the nursery, and answers how many of them ended with an error.

A count, because a count is what a nursery can honestly say. Every child’s error has a type of its own and a nursery holds them as bare handles — that is written down at adopt and is not going to change — so there is no one value to hand back. bounded_nursery turns a non-zero answer into a ChildFailed, which is what a program should be reading rather than this.

The first failure cancels the siblings. A nursery is a unit: the block asked for these fibers together, so one of them failing means the answer the group was computing is not coming and the rest are working on a question nobody will ask. They are cancelled rather than killed, so each stops at its next ! and runs its finalizers on the way out, and every one of them is still waited for.

impl Show for Int

The scalars, so that derive(Show) on a record of them has something to call.

One line each, and each deferring to a to_string that already existed. The two have to agree — Show is what a generic function asks for and to_string is what a caller reaches for directly — and they agree here by one being written in terms of the other rather than by anyone remembering.

A String shows as itself, unquoted. A derived Show is for reading, not for parsing back: User { name: alice } is what a person wants in a log line, and a caller who needs the quotes is writing a serializer, which is std::json’s job and not this trait’s.

fn show(self) -> String
impl Show for Float
fn show(self) -> String
impl Show for Bool
fn show(self) -> String
impl Show for Char
fn show(self) -> String

The character itself, with no quotes.

Show is what a value looks like to a reader, and 'a' printed inside a sentence should read a. The quoted form is how it is written in source, which is a different question and one nothing asks yet.

impl Eq for Char
fn eq(self, other: Char) -> Bool
impl Ord for Char
fn cmp(self, other: Char) -> Ordering

By code point, which is what < on characters means everywhere.

Not a collation order. 'Z' sorts before 'a' because 90 is less than 97, and 'é' sorts after both; a human-facing sort of names wants a locale and a library, and pretending this is one would be worse than not having it.

impl Hash for Char
fn hash(self) -> Int
impl Show for String
fn show(self) -> String
impl Eq for Int
fn eq(self, other: Int) -> Bool
impl Ord for Int
fn cmp(self, other: Int) -> Ordering
impl Hash for Int

An Int is its own hash. Map::slot does the mixing, so there is nothing for this to add.

fn hash(self) -> Int
impl Eq for Bool
fn eq(self, other: Bool) -> Bool
impl Eq for String
fn eq(self, other: String) -> Bool
impl Hash for Bool
fn hash(self) -> Int
impl Hash for String

FNV-1a over the bytes, which is one multiply and one xor each and is what a string hash is supposed to look like. Wrapping on purpose: the overflow is where the mixing comes from.

fn hash(self) -> Int
impl Ord for String

Byte order, which is what < on a String means here.

Not a locale’s order and not code-point order — the first needs a table nobody has bound and the second differs from this one only above the basic plane. What it is is stable, cheap, and the same on every machine, which is what sorting for a program’s own use wants. A human-facing sort wants a collation library and should say so.

fn cmp(self, other: String) -> Ordering
impl Eq for Ordering
fn eq(self, other: Ordering) -> Bool
impl Show for Ordering
fn show(self) -> String

The variant’s own name, which is what a comparison in a log wants to say.

impl<A: Show> Show for Option<A>

Some(3) and None, which is how they are written minus the prefix.

The two most-reached-for types in the language could not be printed. print("saw ${found}") on an Option was refused, and the message advised derive(Show) – on a type in std that the reader does not own and cannot annotate. So the shortest debugging line anybody writes was unavailable, and the advice for fixing it could not be taken.

The variant’s own name, the way [Ordering] does it, rather than the value alone. Some("") and None print differently, which is most of the reason somebody is looking.

fn show(self) -> String
impl<A: Eq> Eq for Option<A>

Both nothing, or both something and the somethings equal.

assert(found == Option::None) could not be written, which is the assertion a test makes about a lookup that should miss. The workaround was assert(Option::is_none(found)), which says the same thing about None and nothing at all about Some.

fn eq(self, other: Option<A>) -> Bool
impl<A: Ord> Ord for Option<A>

Nothing sorts before something, and two somethings by their contents.

The order every language that has this type picks, and the one a sort by an optional field wants: the rows that have the field, in order, after the rows that do not.

fn cmp(self, other: Option<A>) -> Ordering
impl<A: Show, E: Show> Show for Result<A, E>

Ok(3) and Err("no"), for the same reason Option has one.

Both halves need Show, because either may be the one printed and a type that could print only its successes would be the wrong way round for the line somebody is actually writing.

fn show(self) -> String
impl<A: Eq, E: Eq> Eq for Result<A, E>

Same side, and equal contents.

fn eq(self, other: Result<A, E>) -> Bool
impl<A> Show for Redacted<A>
fn show(self) -> String

<redacted>, whatever is inside.

No bound on A: this never looks at the value, which is the point, and requiring A: Show would mean a secret you cannot show is a secret you cannot wrap.

impl Iterator for Range
type Item = Int;
fn next(self) -> Step<Range, Int>
impl<A: Show> Show for List<A>

[a, b, c], which is how the literal is written.

Added because derive(Show) walks fields, and a record holding a List could not derive one – so the type people reach for by default was the type that made a struct unprintable. The bound is on the element, so a list of something unshowable is still unshowable, which is the honest answer.

fn show(self) -> String
impl<A: Eq> Eq for List<A>

Element by element, and length first where they differ in length.

fn eq(self, other: List<A>) -> Bool
impl<A: Ord> Ord for List<A>
fn cmp(self, other: List<A>) -> Ordering

Element by element, and a list that runs out first is smaller.

Lexicographic, which is what every other language means by comparing two sequences and what a sorted list of paths, version segments or keys needs. Length is the tie-break rather than the first question: [1, 2] is less than [1, 2, 0], and [2] is greater than [1, 9, 9].

Consistent with the derived Eq above – two lists compare Equal exactly when eq says they are equal – which is the property that lets sort on a List<List<A>> mean anything.

impl<A> Iterator for List<A>
type Item = A;
fn next(self) -> Step<List<A>, A>
impl Functor for Option
fn map<A, B, 'ef, 'er>(self: Option<A>, f: (A) -> B with 'ef raises 'er) -> Option<B> with 'ef raises 'er
impl Applicative for Option
fn pure<A>(value: A) -> Option<A>
fn map2<A, B, C>(self: Option<A>, other: Option<B>, f: (A, B) -> C) -> Option<C>
impl Traversable for Option
fn traverse<A, B, F: Applicative>(self: Option<A>, f: (A) -> F<B>) -> F<Option<B>>
impl Functor for List
fn map<A, B, 'ef, 'er>(self: List<A>, f: (A) -> B with 'ef raises 'er) -> List<B> with 'ef raises 'er

Each element through f, in order.

A loop and not a recursion, which is the rule every other walk in this file already followed: fold, filter, any, contains, zip and partition are all while with an accumulator, because Khora does not promise tail calls and a frame per element dies on a long list. This one was Cons(f(h), t.map(f)) and went over at about forty thousand – map being, of the lot, the one people reach for first.

Built back to front and reversed, so the answer is in the order the input was and f is called in that order too. That matters when f has effects: map over a list of requests should make them in the order the list is in.

impl Traversable for List
fn traverse<A, B, F: Applicative>(self: List<A>, f: (A) -> F<B>) -> F<List<B>>

Each element through f, with the effects combined and the answers collected.

Two loops rather than a recursion, for the reason map above has one, and two rather than one because the order has to survive being made iterative. There are two orders and they are not the same:

  • f is called front to back, because it may have effects and a traversal over a list of requests should make them in the order the list is in. That is the first loop.
  • The effects combine back to front, because map2 puts its left side’s failures first and the recursion this replaces nested to the right: map2(f(a), map2(f(b), map2(f(c), pure(Nil)))). Combining the other way round reverses the error list, which for Validated – the one type in std built for accumulating errors – is the whole thing it is for. That is the second loop, over what the first collected in reverse.

Doing it in one loop gets one of the two wrong, which is why there are two.

impl Eq for U8
fn eq(self, other: U8) -> Bool
impl Ord for U8
fn cmp(self, other: U8) -> Ordering
impl Show for U8
fn show(self) -> String
impl Hash for U8
fn hash(self) -> Int
impl Eq for U16
fn eq(self, other: U16) -> Bool
impl Ord for U16
fn cmp(self, other: U16) -> Ordering
impl Show for U16
fn show(self) -> String
impl Hash for U16
fn hash(self) -> Int
impl Eq for U32
fn eq(self, other: U32) -> Bool
impl Ord for U32
fn cmp(self, other: U32) -> Ordering
impl Show for U32
fn show(self) -> String
impl Hash for U32
fn hash(self) -> Int
impl Eq for U64
fn eq(self, other: U64) -> Bool
impl Ord for U64
fn cmp(self, other: U64) -> Ordering
impl Show for U64
fn show(self) -> String
impl Hash for U64
fn hash(self) -> Int
impl Eq for I8
fn eq(self, other: I8) -> Bool
impl Ord for I8
fn cmp(self, other: I8) -> Ordering
impl Show for I8
fn show(self) -> String
impl Hash for I8
fn hash(self) -> Int
impl Eq for I16
fn eq(self, other: I16) -> Bool
impl Ord for I16
fn cmp(self, other: I16) -> Ordering
impl Show for I16
fn show(self) -> String
impl Hash for I16
fn hash(self) -> Int
impl Eq for I32
fn eq(self, other: I32) -> Bool
impl Ord for I32
fn cmp(self, other: I32) -> Ordering
impl Show for I32
fn show(self) -> String
impl Hash for I32
fn hash(self) -> Int
impl<K: Show, V: Show> Show for Pair<K, V>
fn show(self) -> String

key: value, which is what it reads as everywhere it is written down.

No brackets around it. A Pair is nearly always inside something that has its own – Dict and Map both print a list of these – and a pair that brought its own would double them.

impl<K: Ord + Show, V: Show> Show for Dict<K, V>
fn show(self) -> String

{a: 1, b: 2}, in key order.

Braces rather than brackets, so a map does not read as a list at a glance. In key order because a Dict is a search tree and entries walks it in order – which means two dictionaries holding the same entries print the same way, and a golden test over one is worth writing. Map cannot promise that and says so.

The key needs both bounds and for different reasons: Ord is what a Dict asks of its keys at all, and Show is what printing one asks. A map whose keys order but cannot print does not print, which is the right answer – there is nothing to write in place of the key.

impl<K: Hash + Show, V: Show> Show for Map<K, V>
fn show(self) -> String

{a: 1, b: 2}, in whatever order the buckets are in.

And that order is arbitrary, so do not write a golden test over one. The type’s own note says entries come back in no particular order and not even a stable one, because growing rehashes everything; printing cannot promise more than the walk it is built on. A Dict prints in key order and is what to reach for when the text has to be comparable.

Sorting here instead would need K: Ord, which would mean a map whose keys hash but do not order could not print at all – and the reason this exists is that a record holding a Map could not derive Show. Trading that back for a tidier order is the wrong way round.

impl<A: Show> Show for Vector<A>

[a, b, c], the same as a List of the same elements.

The two print alike on purpose: which container a value is in is a decision about cost, and a log line that changed shape when somebody swapped one for the other would make that decision visible in the wrong place.

Its elements, not its array. The cells past len hold whatever a pop left behind, and printing those would be printing the vector’s history.

fn show(self) -> String
pub fn assert(condition: Bool)

Fails the test this is written in, if condition is false.

Only allowed inside a test block, and inside one it needs no !. Both halves of that are deliberate: an assertion is the one place a reader of a test already expects control to leave, it is written at every one of them, and a test is not ordinary code.

pub fn print(value: String)

Writes a line to standard output.

Declared without a body, so it is an extern: the backend dispatches it on the argument’s type, because there is no prelude yet to declare three differently-typed printers. It is not an effect, which it eventually should be — writing to a console is exactly the sort of thing a capability is for.

pub fn todo<A>() -> A

A hole where the code is not written yet.

It answers whatever is wanted, so it fits anywhere. An unwritten match arm has to agree with whatever its neighbours produce, so this is generic in its result and todo() type-checks in a String arm and an Int arm alike, leaving the program compiling while a case is unfinished.

Generic rather than -> Never because a body that is a Never call has no value for the backend to return; the runtime symbol under it has that type, and a caller substitutes its own.

Running one stops the program the way every other trap does, with a message and status 134. That is the point rather than a limitation: a placeholder that returned a plausible value would let a half-written program answer, and a wrong answer is the one outcome worse than a refusal.

match colour {
Colour::Red => "red",
Colour::Green => todo(),
}
pub fn join_all<A: Share, 'er>(fibers: List<Fiber<A, 'er>>) -> List<A> raises 'er

Every fiber’s answer, in the order the handles were given.

The gap a nursery cannot close. Fibers::wait waits for its children and discards what they computed, and it has to: a nursery holds Fiber<(), 'er>, because an operation cannot be generic in a type – only in a row – so the answer is fixed at () where the handles are held. That is written down at Nursery and it is not going to change.

This is a plain function rather than an operation, so it can be generic in A, which is the whole reason it can exist. Fan out with Fiber::spawn, keep the handles, and ask for the answers:

let prices = join_all(List::map(symbols, fn s => Fiber::spawn(fn () => quote(s)!)))!;

The first failure comes out, and the rest are still waited for. The raise unwinds past the handles that have not been joined yet, and letting a handle go waits for its fiber – which is where structured concurrency comes from and is why this needs no cleanup of its own. So a fan-out where one branch fails is a raise, not a leak, and not a fiber still running after the caller has moved on.

In handle order rather than completion order. A caller who wanted the fastest first wanted a channel, and one who wanted the answers lined up against the requests – which is nearly everybody – wanted this.

A: Share is Fiber::join’s own bound and not a new one: the answer was computed on another fiber and has to be safe to hold from this one.

pub fn nursery<A, 'ef, 'er>(body: () -> A with { 'ef | nursery: Nursery } raises 'er) -> A with 'ef raises 'er + ChildFailed

Runs body with a nursery, and does not return until its children are done.

Structured concurrency, and the structure is the region’s: a fiber cannot outlive this block, because leaving it — at the end, on a return, or with a raise passing through — releases the nursery, and releasing a nursery stops what is still running.

A named function or a lambda; both work. A named function receives what its with clause names as evidence parameters, so it can be handed here directly and called with the nursery this function installs:

fn serve<'ef>() -> () with { 'ef | nursery: Nursery } { .. }
nursery(serve)!

nursery(fn () => serve()!) is the same thing. A lambda resolves a capability lexically where it can and requires it where it cannot, so this one’s type becomes () -> () with { nursery: Nursery } and the body()! below supplies it from the handler just installed. That is the rule in the capability-passing design note, and the reason for it is that eta-expansion must not alter what a program means.

And the body may name the nursery, which for a while it could not:

nursery(fn () => {
nursery.adopt(Fiber::spawn(work));
nursery.adopt(Fiber::spawn(more));
})!

Inside the body, nursery is the capability rather than this function: the label is the lambda’s parameter in all but spelling, and shadows this function the way a parameter shadows anything else.

pub fn bounded_nursery<A, 'ef, 'er>(limit: Int, body: () -> A with { 'ef | nursery: Nursery } raises 'er) -> A with 'ef raises 'er + ChildFailed

The same, holding at most limit children at once.

Which one to reach for is a question about where the work comes from. A nursery over a known handful of tasks — three lookups in parallel, a fan-out over a list you are holding — should be unbounded, because the count is already bounded by the thing you are iterating. A nursery fed by the outside world should not be, because then the count is somebody else’s decision.

A fiber is an operating-system thread today, so unbounded there means a ceiling met by exhausting memory. Bounded means one met by adopting more slowly, which for a server means accepting more slowly, which the listening socket’s backlog is already built to absorb: overload becomes latency instead of collapse.

pub fn scoped<A, 'ef, 'er>(body: () -> A with { 'ef | scope: Scope } raises 'er) -> A with 'ef raises 'er

Runs body in a fresh region, discharging its scope requirement.

Row subtraction made concrete: scope is present in the body’s requirement row and absent from the result’s. The region is an ordinary binding of this function, so it ends when this function does — including when body raises and the error passes straight through.

A named function or a lambda, for the reason on nursery.

pub fn acquire<A, 'ef>(value: A, release: (A) ->()) -> A with { 'ef | scope: Scope }

Acquires a value and registers its release in one step.

The generic half of Scope, kept out of the effect so the operation can stay a plain closure. release is called with value when the region ends.

pub fn attempt<A, E, 'ef>(body: () -> A with 'ef raises E) -> Result<A, E> with 'ef

Runs body and makes its failure a value.

The error channel and Result are the same thing said two ways — a tagged return already is “an error or a value” — and this is where a caller chooses which way to read it. The row of body is discharged, so what comes back cannot fail again.

This is also what makes retrying possible: a policy that runs a computation a second time has to be able to see that the first time did not work, without knowing what it was doing.

One error type, and that is a real limit. E is a type rather than a row, so a body raising IoError + Denied has nowhere to go through here:

error: this argument: `Denied` is not accounted for here. This takes one
error type and the body raises `IoError` and `Denied`; there is no
type that means "either of these", so handle them with `catch`
instead

The limit is not an oversight. Result<A, E> needs one E, and Khora has no anonymous sum to name “either of these two” — so there is nothing for a two-type row to collapse into. Naming a union would mean declaring a type for every pair of failures a program happens to combine, which is worse than the thing it fixes.

catch is the answer, because it matches per type and never has to name the union:

let answer = fetch(url)! catch {
IoError::NotFound(_p) => fallback(),
Denied(_p) => refuse(),
};

A row of one is what attempt is for, and it stays the shorter way to write it.

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