Skip to content
You are reading next, which describes the unreleased compiler. Khora is unstable before v1: syntax, standard-library APIs and behavior may change. The current release is documented under v0.4.

Traits

A trait is what generic code is allowed to assume, and an impl is a type’s answer to it. Both are checked where they are written.

pub trait Named {
fn name(self) -> String;
}

General form:

pub? trait Name<TypeParams>? (: Trait + Trait ...)? {
TraitItem*
}

A trait item is a function declaration or associated type declaration.

Required method:

pub trait Named {
fn name(self) -> String;
}

Default method body:

pub trait Named {
fn name(self) -> String;
fn greeting(self) -> String {
"Hello, ${Named::name(self)}"
}
}

A semicolon declares a required signature. A block supplies a default implementation.

pub trait Ord: Eq {
fn cmp(self, other: Self) -> Ordering;
}

Several supertraits use +:

pub trait Persisted: Eq + Show {
fn id(self) -> Int;
}

An implementation of the child trait must satisfy the supertrait requirements.

Declare an associated type. Iterator in std::core declares two, and they answer different questions — what the iteration yields, and what pulling from it requires:

pub trait Iterator {
type Item;
type Effects;
fn next(self) -> Step<Self, Self::Item> with Self::Effects;
}

With a bound:

pub trait Indexed {
type Key: Eq + Show;
fn key(self) -> Self::Key;
}

Supply them in an implementation. Effects = {} is an iterator over something already in memory, so next asks its caller for nothing:

impl Iterator for Users {
type Item = User;
type Effects = {};
fn next(self) -> Step<Users, User> {
// ...
}
}
impl Named for User {
fn name(self) -> String {
self.name
}
}

General form:

impl<TypeParams>? TraitType for TargetType {
TraitItem*
}

A trait implementation provides the methods and associated types required by the trait.

Coherence, and the orphan rule that is not there yet

Section titled “Coherence, and the orphan rule that is not there yet”

One impl of a trait for a type is the rule, and the compiler enforces it across the whole compilation — the package’s own modules, the sources of the packages it depends on, and std. Two impls in one file are refused where they are written:

error: `Codec` is already implemented for `A`; there can be only one impl of a
trait for a type

and a second impl Codec for A in another module names the module that already has one:

error: `Codec` is already implemented for `A` in `store`; there can be only one
impl of a trait for a type in a program

The file whose path sorts first keeps the impl and the other is what gets reported, so which of two modules is refused does not depend on the order the files were read in. A type’s identity is its name and the module that declared it, so two types both named Entry are two types and each may have its own impl Codec.

There is still no orphan rule, with one exception: Share, which only the module declaring a type may implement for it. Nothing else stops you writing someone else’s trait for someone else’s type. What has changed is that a second impl is now an error you are shown rather than a silent choice of whichever one was merged first. The only impl the compiler still cannot see is one in a package this build does not include, so keep “the declaring package owns the impl” as a convention: implement a trait for a type you declare, or a type you declare for someone else’s trait.

impl<A: Show> Show for Box<A> {
fn show(self) -> String {
Show::show(self.value)
}
}

The type parameters after impl are scoped to the implementation block.

An impl without for defines methods belonging directly to the target type:

impl User {
fn normalized_name(self) -> String {
self.name |> String::trim
}
pub fn display_name(self) -> String {
User::normalized_name(self)
}
}

General form:

impl<TypeParams>? TargetType {
fn ...
pub fn ...
}

A public inherent method uses pub. Trait methods are reached through the trait contract rather than separately exported implementation members.

fn equal<A: Eq>(left: A, right: A) -> Bool {
left == right
}

Several bounds:

fn render<A: Eq + Show>(value: A) -> String {
Show::show(value)
}

Bounds are trait paths separated by +.

Inside a trait, Self names the implementing type:

pub trait Combine {
fn combine(self, other: Self) -> Self;
}

A trait’s methods resolve on every type that implements it, imported or not. An implementation is a property of the type, and value.method(..) asks the type — so this compiles in a module that imports neither Show nor Eq:

module app::main;
import std::core::{print};
pub fn main() -> Int {
let same = 1 == 1;
let text = 42.show();
if same { print(text); }
0
}

Operators follow the same rule: == is Eq::eq, and it works on any type that implements Eq without the name being brought in.

What an import buys is the name. Show has to be imported wherever the word Show is written — as a bound in <A: Show>, in a derive(Show) clause, or as the prefix of a trait-qualified call. for needs Iterator and Step in scope for exactly that reason: it desugars to Iterator::next, and a path is a name being looked up. One consequence worth knowing before it surprises you: a file that imports Eq and then only ever writes == never writes Eq, so unused-import reports the import, correctly.

A type may implement more than one trait that declares a given method name. When it does, method syntax has nothing left to choose with:

error: `map` is declared by `Functor` and `Iterator`, and `List<Int>` implements more than one

A type’s own method outranks a trait’s, so this only arises where the type has none. List implements both Functor and Iterator and both declare map, which is exactly the collision above — and xs.map(f) is not ambiguous, because List has a map of its own. It is the eager one: a list in, a list out. Adding a trait to a program must not silently change what an existing call does, and the same rule is what keeps an abstraction most programs never mention from making the most ordinary line in the language an error.

Where a type genuinely has no method of its own, name the trait. A trait-qualified call passes the receiver as the first argument and settles which one is meant:

module app::main;
import std::core::{Functor, List, Show, print};
pub fn main() -> Int {
let xs = List::Cons(1, List::Cons(2, List::Nil));
let ys = Functor::map(xs, fn (n) => n + 1);
print(Show::show(ys));
0
}

Iterator::map(xs, fn (n) => n + 1) picks the other one, and gives a lazy Mapped rather than a List — which is why the two cannot simply be merged, and why List::map had to choose. The trait has to be imported for this form, since this is the case where the name is written. Without the import the call is refused with cannot resolve `Functor::map` in this scope.

Functor::map is a trait path, not a module path. Modules and packages says a module path cannot prefix an expression, and that still holds — std::core::map(..) does not resolve. :: after a trait or type name reaches an associated item, which is what this is.

derive(Eq, Ord, Show, Hash, Decode, Encode)
pub type User = {
pub id: Int,
pub name: String,
};

The compiler can derive those six structural traits when every field supports the requested behavior. A derive(...) clause appears immediately before its type declaration. Decode and Encode are std::schema’s: the declaration is the schema, and User::schema() reads one from untrusted input.

pub trait Convert<A> {
fn convert(self) -> A;
}

Trait parameters use the normal generic parameter syntax, including bounds, const parameters, and variance where meaningful.

See Generics for parameter forms and Declarations for the top-level grammar shared by traits and implementations.

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