Traits
On this page
Note: All of this capability is implemented, except for actor trait, whose shape the grammar accepts and which the compiler does not build, as actors are not declared in source yet, and a requirement taking Self by value (below). mod trait is implemented, and described with modules.
Similar to struct, the definition for a trait type specifies named methods (and, possibly, fields). For example:
trait Id {
pub id u32;
pub fn print();
}
However, trait types accomplish a very different purpose than struct types. Instead of specifying the complete field layout and behavior of concrete data, trait types abstractly specify a common pattern of shared behavior (methods) and state (fields) across multiple concrete types. By this understanding, the Id trait above expresses the idea that there are other types out there which define a print method (that accepts no arguments and return no values) as well as an unsigned integer field named id.
Because traits abstract over other types, their use differs from concrete types (like structs) in two important ways:
- One cannot explicitly create a value whose type is a trait. Thus, it is not possible to construct a value of this type (e.g., imm node = new Id(12) is illegal). Instead, you create a concrete-type value, and then you view that concrete value through the lens of a compatible trait.
- A trait usually specifies methods that have no implementation logic, as the print method above demonstrates. When a method is called on a trait-based value, it will automatically substitute in the method implementation logic defined by the concrete value's type.
A trait is an abstract kind#
A trait is not a kind of its own. trait is a modifier on a kind, and a trait is the abstraction of whatever kind it modifies. The full spelling of the declaration above is:
struct trait Id {
pub id u32;
pub fn print();
}
trait Id and struct trait Id are the same declaration, written two ways. The short form is the one you will normally write, and it means the struct family, because that is what a trait has always abstracted over.
Two more kinds take the modifier. A mod trait is the interface a module conforms to, with functions and globals as its members, and its defaults copied into each module that says it conforms: see modules. An actor trait is the abstraction of an actor; it is accepted by the grammar so that its spelling is settled, and the compiler reports it as not built yet.
The kind keyword is what says which family an abstraction belongs to, and composition requires the families to match: a struct composes a struct or a struct-trait, and an actor will compose an actor or an actor-trait. Nothing else on the declaration says which family it serves, and nothing about it is inferred.
enum does not take the modifier, and the omission is deliberate. An enum is concrete: its identity is the set of variants it declares, so anything you could hold behind an abstraction of one either is that set — and so is the enum — or is open, and so is a trait. enum trait is rejected.
Complying with a trait#
A concrete type complies with a trait when it declares everything the trait requires: a method of each required signature, and a field of each required name and type. Compliance is noticed structurally. A type that happens to declare what a trait requires may be viewed through that trait, whether or not it has ever heard of it — which is what lets you apply a trait to a library type you did not write, and saves you from defining interfaces defensively in case you need them later.
Declaring it, with is#
A type may also say so in its own declaration:
trait Id {
pub id u32;
pub fn describe(self &) u32;
}
struct Node is Id {
pub id u32;
pub value u32;
pub fn describe(self &) u32 {id + value;}
}
This is the only place in Cone where a subtype relationship is asserted rather than noticed. The compiler verifies the compliance where it is written, and rejects the declaration if it does not hold.
It is the same is you already know. p is Mobile asks of a value what struct Node is Id asserts of a type, and X is Y means “X is a Y” in both. The difference is the operand, not the question: a value's variant is dynamic, so the value form is answered at runtime, while a type's relationships are fixed, so the declaration form is settled where it is written. One predicate over two kinds of operand — not two mechanisms that happen to share a word.
The obvious objection is that structural compliance makes the declaration redundant. It does not, for two reasons.
A marker trait is empty, so nothing structural can ever notice it. Plenty of useful traits carry no fields and no methods at all — they are testable invariants, adjectives a type either claims or does not. There is no such thing as a structural match to a trait that declares nothing: every type would match it, so the match would say nothing at all. Such a trait can only be carried by the types that nominate it, which is what is is for. Nothing else in the language can express it.
The compiler knows these marker traits of its own, reached from every module without an import, and none has members:
- Move and Copy. Every type is exactly one of the two. Unlike a marker trait you write, the compiler also grants one to every type from what it can see: a finalizer, a field that moves or an owning reference that cannot be aliased makes a type Move, and a type with none of these is Copy. A type may still nominate either. struct Handle is Move makes a type move that would otherwise copy; is Copy asserts that it copies, and is rejected on a type that turns out to move, including one that also says Move.
- RegionRef, which a region's reference struct declares (Regions). A region ref may also declare Move, as So does: struct So is RegionRef, Move.
- AtomicValue, which marks a value changed only by atomic operations, even where it is imm: core's Atomic[T] declares it (Atomic values). A type declaring it moves.
- Integer, which the compiler grants to the integer types, i8 to i64, u8 to u64, isize and usize, for a generic to constrain on: where T is Integer.
- Pointer, which the compiler grants to the raw pointer types, *T whatever T, and not to a reference, for a generic to constrain on: where T is Pointer.
- Sendable, which the compiler grants to every type whose values may cross to another thread, for code that hands a value to one to constrain on: thread.start and the sync package's channels ask where T is Sendable. What it refuses is in Race condition safety. A type holding raw pointers it shares safely may declare it, which the compiler takes on trust; an instance of a generic type declaring it is Sendable only where its type arguments are.
- Shareable, Sendable's sibling, which the compiler grants to every type a borrow of which several threads may hold at once: no counted owner whose count is not atomic, no traced reference, no &mut and no unmarked reference to a trait or a callable in it, where a borrow of it reaches. A parallel each body may name only such values from outside its loop, and a generic constrains on it, [F fn() + Shareable] (Sendable and Shareable). A type may declare it, taken on trust, like Sendable. A reference to a trait or a callable may say what the value behind it is: &<Shape + Shareable, So[fn() + Sendable].
Their names are taken in every module, as a built-in type's are: a module-level declaration named like one, such as struct Integer, is an error saying the name is a built-in trait's.
These are traits rather than attributes because each is a fact other code may ask about or constrain on; an attribute, such as @opaque, is an instruction to the compiler about representation or linkage that no code asks about. A generic asks with a constraint, where T is Copy (Generic constraints), which a marker answers only by the compiler's grant or a declaration, and any other trait also by a type that fits it. (The other asking side, a compile-time test, is not built yet.)
And a declaration forces compliance at the moment of declaring, rather than later at the point of use. If a type is meant to satisfy a trait, saying so is what makes the compiler tell you the day you break it, at the type itself, instead of the day something far away tries to view a value through the trait and fails there.
The two kinds of compliance exist alongside each other on purpose. Structural compliance keeps traits applicable to types that never anticipated them; is lets a type commit, and makes the empty trait expressible at all.
What the declaration takes, and what it gives#
- A comma-separated list. A type may declare is against any number of traits: struct Ledger is Audited, Sealed, Move.
- The trait contributes no fields. What a trait's fields state is a requirement, and the type declares them itself — the same names, the same types, in the trait's order, ahead of any field of its own. That prefix is what lets a plain &Id reference read the trait's fields straight out of the value, and what makes a same-size conversion to the trait a pure recast rather than a copy. Declaring them in another order, or behind a field of your own, is rejected.
- Only the first trait in the list may require fields, since only one set of them can occupy the front of the layout. The rest must require none — which is no restriction in practice, because a field two traits want is declared once and satisfies both.
- A method the trait implements and the type does not is inherited free. A trait may give a method a body, which becomes the default for any complying type that does not declare its own. Declaring it is the primary case; taking the trait's implementation is the fallback. Either way the method is the complying type's own, called on a value or named through that type: Node.describe. The default may be generic (fn map[F fn(x T) U, U](self &, f F) ...): a call on the type makes an instance for it, with self the type. A type's own method replaces a shared one of the same name, whatever its shape (its parameters and result need not be the trait's): Zip declares its own zip that gives a flat triple, where Iterator's shared zip gives a pair, and the chunk cursors declare theirs to name their lifetime. This holds for a default that is generic, has a where clause or is inline, which fill no slot of the trait's virtual reference. A default that fills a slot must still be met by a method of its signature, as a requirement is. The type's own method is found by a direct call, and by generic code over the trait too (a generic function is made for the type it is given, so it.zip(other) in fn f[I Iterator[T], ...](it I, ...) calls the Zip's own when I is a Zip); only a type without its own gets the shared one. Behind a virtual reference (&<mut Iterator[T]) neither is reachable, since a generic default has no slot. A trait's method named through the trait, Id.describe, is an error, since the trait keeps no copy of it; only a trait's static functions are reached that way.
- A trait may itself declare is against another trait, layering a deeper abstraction over a shallower one. It complies with the field requirement the way any type does, by declaring the fields.
- It folds nothing. There is no use clause on an is: a trait is an abstraction, so there is no value for a folded name to be reached through. Delegation is what a field's own use clause is for.
- An enum and its variants declare it too. enum Shape is Named gives every variant the trait's requirements and defaults, and a variant may name traits of its own beyond its enum: struct Square is Measured, written inside the enum. Neither list has a base, so neither may name a trait that requires fields; Complying with a trait has the details. A match on an enum may name such a trait as a pattern, which matches the variants that comply (Trait patterns).
- Every name in the list is an open trait. An enum is closed: its variants are all declared inside it, so naming one anywhere in an is list, first or later, is rejected.
- It is the one way to take a trait in. The retired mixin statement, which did the same from inside a type's body, is an error that points here.
Subtypes in Cone are always abstractions. A concrete type is never a subtype of another concrete type, so the base of an is must be a trait; naming a struct is rejected. Reusing another concrete type's code is a separate question, answered by delegated inheritance through a field, and by enrichment with extends over a concrete base. Neither grants subtyping, and a type may write extends and is together, in either order: they assert different things.
Hash, and the hash the compiler supplies#
A key of a table, or anything else looked up by what identifies it, says what identifies it by feeding a Hasher, and the Hasher turns what was fed into a number. The two are kept apart, as Rust's Hash and Hasher are and Swift's hash(into:) is: a type decides what it feeds, and whatever holds the keys decides the algorithm, so changing the algorithm changes no key type. Both are core's, reached from every module without an import. The trait is
pub trait Hash {
pub fn hash(self &, h &mut Hasher);
}
and the rule that ties it to equality is that if two values are == they feed a hasher the same things. So a type that declares is Hash must have a ==, and is refused without one.
A struct that declares Hash and writes no hash is given one by the compiler, as != is derived from ==: it feeds each field, in the order declared.
struct Id is Hash {
pub slot u32;
pub gen u32;
pub fn `==`(self, o Id) bool {slot == o.slot and gen == o.gen;}
}
mut h = Hasher.seeded();
id.hash(&mut h); // feeds slot, then gen
imm number = h.finish();
It feeds the fields, never the struct's bytes, because the bytes between fields are undefined. It is refused, at the field, where a field has no hash that agrees with its ==: a float (NaN is not equal to itself and -0 is equal to 0), or any type that is not Hash, such as a struct that does not declare it, a reference, a slice, a tuple, an array or a pointer. A type that wants such a field in its key writes its own hash, which feeds what identifies it:
pub fn hash(self &, h &mut Hasher) {
h.writeU64(u64.from(slot) | (u64.from(gen) << 32u64)); // two halves, mixed once
}
Every integer type and bool is Hash, feeding its bits, and so is an owner of text, a So[str] or an Rc[str], feeding the bytes as the str it lends does; a float is not. A value whose length varies feeds a terminator after its bytes, h.writeStr(bytes), which is the bytes then the byte 0xFF, so that where one ends and the next begins is part of what is fed: ("ab", "c") and ("a", "bc") differ as the two parts of one key. A fixed-size run of bytes is h.writeBytes(bytes).
The Hasher is a fast hash of the wyhash family: each word fed is xored into its state and multiplied (mulFold), and bytes go through wyhash itself. Hasher.seeded() starts it from a seed drawn at random from the operating system once for the process (processSeed() is that number), so which keys collide is different on every run and no one outside can tell: the defence against keys chosen to collide. Hasher.withSeed(n) starts it from a seed of your own, and then the same keys give the same numbers on every run and build, for a table whose order must be reproducible.
Iterator and Iterable#
Two more of core's traits name the shapes a sequence of values is walked by, for code that is generic over them. A type is an Iterator[T] by having next, and an Iterable[T, I] by having iter. Neither is declared: a type with the method complies, as the cursors of str and of every collection do, and so does a struct of your own.
pub trait Iterator[T] {
pub fn next(self &mut) Option[T]; // the next value, None once there are no more
}
pub trait Iterable[T, I] where I is Iterator[T] {
pub fn iter(self &) I; // a cursor over what it holds, without consuming it
}
A function asks for one as a bound, decided where it is called and compiled for the type's own methods, or as a virtual reference (&<mut Iterator[i64]), which takes any cursor through one compiled function:
fn count[T, S Iterator[T]](it &mut S) usize {
mut n = 0usize;
while true {
match it.next() {
case imm v Some {n = n + 1usize;}
case is None {break;}
}
}
n;
}
fn total[S Iterator[&i64]](it &mut S) i64 { ... } // a cursor that lends numbers
fn walk[T, I, C Iterable[T, I]](c &C) where I is Iterator[T] { ... c.iter() ... }
T is what next gives, so a cursor that lends its values (a Deque's items(), a Dict's keys()) is an Iterator[&V]. The &V of a bound names no lifetime, and a cursor whose next returns the lifetime its own struct declares (Option[&'a V]) meets it, as a method with no other parameter has nothing to store through its receiver. The cursor's own signature is what a bound's call reads, so borrows given by two calls of next may be held together; through a virtual reference the trait's signature is read, which ties a result to the whole cursor. Iterable takes its cursor's type as a second argument, I, because a trait cannot yet name a type its implementer chooses; both arguments are found from the type passed. A type with no next (or iter), or with one that gives another type, is refused where the function is called.
Fitting is by shape, and the trait's shared methods come with the declaration. A type that says struct Countdown is Iterator[i64] has, beside its own next, the steps that work on a whole sequence, map, filter, take, skip, fold, any, all, count, find, sum, min and max (Each and Iteration), which the trait writes once as generic defaults. A type with a next and no declaration meets a bound on the trait all the same and has none of them. A generic default has no slot behind a virtual reference (&<mut Iterator[i64] calls next), and a default that exists only where a condition holds (sum wants number items) has none either.
A closure literal fills a trait with one required method#
A trait whose work is one method, such as a distance function or a comparison, would need a struct written for each use. A closure literal given where such a trait is wanted does that instead: it is the method. The trait must have exactly one required method, a method written without a body, and no field. A method with a body is a default, and the literal's type inherits each default as any type complying with the trait does, so the defaults call the literal's body through the required method:
trait Shape {
pub fn distance(self &, p V) f32; // the one required method
pub fn nearness(self &, p V) f32 { 100. - self.distance(p); } // a default
}
fn depth[S Shape](shape &S, p V) f32 { shape.distance(p) * 2.; }
fn lend(shape &<Shape, p V) f32 { shape.nearness(p); }
depth(q => q.x * q.x + q.y * q.y - 5., p); // the static form: p is (3, 4): 40
lend(q => q.x + q.y, p); // the dynamic form: 100 - 7: 93
imm k = 10.;
imm own = new So[Shape](fn(q V) [off = k] f32 { q.x + off; });
own.distance(p); // 13
own.nearness(p); // 87
The places a trait is wanted are a generic constraint ([S Shape], taken as S, &S or &mut S), a borrowed virtual reference (&<Shape, &<mut Shape, which lend the literal for the statement), and an owner made with new (So[Shape], Rc[imm, Shape]). It applies only to a literal, written at the point the trait is expected, so no struct of yours meets a trait any differently. A generic trait's instance as the constraint ([S Shape[T]]), and a literal as a field's initial value or a variable's, are not built.
- The signature is the method's, without its receiver. The literal's parameter types may be left out, and if written they must be the method's exactly, as must a written return type, or the error names the method: "the trait's distance takes V there". The number of parameters must match.
- The literal's method has the method's name, so a state entry or captured variable of the same name is the ordinary duplicate-name error.
- A trait with two required methods, none, or a field is refused, naming what it has. A trait with only defaults has nothing to fill.
self: a literal that changes state. As for any closure, the method's self follows the body: self & for a body that only reads, self &mut for one that changes what it holds or borrows. The literal is held to the trait as any implementer is, which may not ask for a stronger self than the trait's method declares. So a literal that changes state fills a trait whose method says self &mut, and is refused where the method says self &, under every holder, &<Shape, &<mut Shape, So[Shape] and a generic's S alike, with an error that says to declare the trait's method self &mut or to not change the state. A literal that only reads meets a method of either kind.
trait Counter {
pub fn bump(self &mut, step i32) i32;
}
fn bumpTwice(c &<mut Counter) i32 { c.bump(1); c.bump(10); }
bumpTwice(fn(step i32) [mut n = 5] i32 { n += step; n; }); // 6, then 16: 16
// ERROR on a trait that says 'fn distance(self &, ..)':
// depth(q => { total += q.x; total; }, p); // needs 'self &mut' but Shape's distance takes 'self &'
This is a rule about the reference, not only the trait: a literal that changes state, given as &<mut Shape and narrowed to a &<Shape, would change state through a reference promised to read.
Self in a trait#
Inside a trait, Self stands for the type complying with it. A requirement that takes another value of the same type
trait Same {
pub fn same(self &, o &Self) bool;
}
is met by a same method taking a reference to the complying type, o &Self or o &Node within Node, whether compliance is noticed by a generic constraint or declared with is. A default method a type takes from the trait means that type by Self too. Three things differ:
- A virtual reference cannot be made to such a trait (Type Restrictions): the value behind it may be of any complying type, so there is no one type for Self to be. &<Self is the exception, a virtual reference to the trait itself, met by a method taking &<Same.
- An enum's requirement is met by each variant as it is written. A call through a reference to the enum reaches the variant by its tag, with an argument that may hold any variant, so Self there is the enum: a variant meets o &Self with o &Number.
- Not built yet: a requirement taking Self by value, b Self or a by-value self, is refused at a trait whose implementations may differ in size, as a trait held by value.
Traits make possible several valuable kinds of polymorphism, particularly:
- Trait-based variants, which enable definition of multiple variant structs that share some base trait's common fields and methods.
- Virtual reference interface, enabling the use of methods (and fields) on values whose concrete type can only be determined at runtime.
- Generic constraints, which can be use to restrict which parametric types can be successfully substituted in to instantiate (monomorphize) a generic type.