Traits & Impls
Traits declare requirements (fields and method signatures); impl blocks
attach methods to a concrete type or declare conformance.
Trait Definitions
// Fields only
pub trait Named {
name: String
}
// Fields and methods
pub trait Shape {
color: String
fn area(self) -> I32
fn perimeter(self) -> I32
}
// Methods only
pub trait Drawable {
fn draw(self) -> Boolean
fn visible(self) -> Boolean
}
pub trait Identifiable {
id: I32
}
// Trait composition (inheritance)
pub trait Entity: Named + Identifiable {
createdAt: I32
}
// Generic trait
pub trait Collection<T> {
items: [T]
}
Trait Rules:
- Fields listed without
fnare structural requirements (the struct must have them) fnsignatures listed without a body are method requirements- A line break separates two members of a trait. A comma is also allowed
- Trait composition (
+) combines requirements from multiple traits - A type satisfies a trait by providing all required fields and all required methods
- A trait method may declare type parameters of its own. See Generic Trait Methods
Generic Trait Methods
A trait method may declare type parameters of its own, after its name. Each impl of the trait declares the same number, in the same order:
pub struct Meters { v: I64 }
impl From<Literal<I64>> for Meters {
fn from(value: Literal<I64>) -> Meters { Meters(v: value.value) }
}
pub trait Conv {
fn convert<T: From<Literal<I64>>>(self) -> T
}
pub struct Probe { n: I64 }
impl Conv for Probe {
fn convert<X: From<Literal<I64>>>(self) -> X { 2 }
}
fn through<U: Conv>(u: U) -> Meters {
u.convert() // T = Meters, from the return type
}
pub let m: Meters = Probe(n: 1).convert()
pub let d = Probe(n: 1).convert<Decimal>()
pub let t = through(u: Probe(n: 1))
Rules:
- An impl method declares as many type parameters as the trait method. The names may differ: the check reads them by position
- After that renaming, the signatures must be the same
- Each bound of the impl method must be a bound of the trait method too. The impl method may have fewer bounds, because a call through the trait meets each bound of the trait method. A bound that only the impl method has is a mismatch: a call through the trait does not check it
- A mismatch is the error
TraitMethodSignatureMismatch(E101). The message shows both signatures with their type parameters - A call gives the type parameters their types as for a method in an
impl block: by
<...>, by an argument, or by the position of the call. This holds on a value and on a type parameter with a bound
Impl Blocks
Impl blocks add methods to a struct (inherent impl) or declare trait conformance (impl Trait for Struct).
Inherent impl: methods belong to the struct:
pub struct Counter {
value: I32
}
impl Counter {
fn increment(self) -> I32 {
self.value + 1
}
fn reset(self) -> Counter {
Counter(value: 0)
}
}
pub fn run_checks() {
let c = Counter(value: 4)
assert(condition: c.increment() == 5)
assert(condition: c.reset().value == 0)
}
The self parameter
self is the receiver of a method. It exists only in a method that
declares it:
selfmust be the first parameter. In another position it is aParseError.- A method without a
selfparameter is a static method. A read ofselfin it is the errorUndefinedReference. - A free function has no receiver, so a
selfparameter there is aParseError. selfis a keyword, so it cannot name a binding, a parameter, a field or a function.
self takes a convention, as each parameter does: self, mut self
or sink self. See
Functions / Parameter Conventions.
pub struct Counter { value: I32 }
impl Counter {
fn get() -> I32 {
self.value // error: this method declares no `self`
}
}
Static methods
A call on the type, Type.method(...), reaches a static method. This
is the form of Swift. A call through a value of the type, c.zero(),
reaches it too:
pub struct Counter {
value: I32
}
impl Counter {
fn zero() -> Counter {
Counter(value: 0)
}
fn starting(at: I32) -> Counter {
Counter(value: at)
}
fn reset(self) -> Counter {
Counter.zero() // inside the impl, name the type too
}
}
pub fn run_checks() {
assert(condition: Counter.zero().value == 0)
assert(condition: Counter.starting(at: 5).reset().value == 0)
}
Rules:
Name.method(...)is a static call whenNameis a struct. On an enum,Name.member(...)is a variant, and it is a static call only when the enum has no variant of that name:Optional.from(value: 3)- A call on the type reaches only a method with no
self. A method that takesselfneeds a value:Counter.get()is the error E151 (NotAStaticMethod) - A bare name names a function, not a method, as in Rust: inside the
impl, a bare
zero()is the errorUndefinedReference. WriteCounter.zero() - On a generic struct, the path gives the type arguments:
Box<I32>.empty(). With no<...>, the arguments give them, as for a generic function:Box.of(v: 3)is a call onBox<I32>. A call that gets no type for a parameter of the struct is the errorMissingGenericArguments
pub struct Box<T> { value: T? }
impl Box<T> {
fn of(v: T) -> Box<T> { Box(value: v) }
fn empty() -> Box<T> { Box(value: nil) }
}
pub fn run_checks() {
assert(condition: Box.of(v: 3).value.is_some())
assert(condition: Box<I32>.empty().value.is_none())
}
pub struct Counter { value: I32 }
impl Counter {
fn get(self) -> I32 { self.value }
}
pub fn f() -> I32 {
Counter.get() // error E151: get takes self
}
Self
Self names a type. Its meaning depends on where it is:
- In an impl block,
Selfis the target type. Inimpl Box<T>,SelfisBox<T>.Self(value: v)andSelf.zero()name the target type too. - In a trait,
Selfis the type that implements the trait. An impl givesSelfits own type: inimpl Zero for Point, the trait'sfn zero() -> Selfisfn zero() -> Point. - In other places,
Selfis the errorUndefinedType.
A call through a bound reads Self as the type parameter: through
T: Zero, x.twice() returns a T. A call on the type parameter
itself, T.zero(), reaches a static method of the bound. A method that
takes self needs a value: T.twice() is the error NotAStaticMethod.
pub trait Zero {
fn zero() -> Self
fn twice(self) -> Self
}
pub struct Point {
x: I32
}
impl Zero for Point {
fn zero() -> Self { Self(x: 0) }
fn twice(self) -> Self { Point(x: self.x * 2) }
}
fn doubled<T: Zero>(item: T) -> T { item.twice() }
fn origin<T: Zero>() -> T { T.zero() }
pub fn run_checks() {
assert(condition: doubled(item: Point(x: 3)).x == 6)
assert(condition: origin<Point>().x == 0)
}
One type can implement two instances of one generic trait. When their methods differ in a parameter type, the type of the argument chooses the impl, for a static call too:
pub trait Make<T> {
fn make(value: T) -> Self
}
pub struct Tag {
n: I64
}
impl Make<I64> for Tag {
fn make(value: I64) -> Self { Tag(n: value) }
}
impl Make<String> for Tag {
fn make(value: String) -> Self { Tag(n: 0I64) }
}
fn build<T: Make<I64>>(n: I64) -> T { T.make(value: n) }
pub fn run_checks() {
assert(condition: Tag.make(value: "x").n == 0I64)
assert(condition: build<Tag>(n: 4I64).n == 4I64)
}
Impl Trait for Type
Declare that a type conforms to a trait using impl Trait for Type:
pub trait Named {
name: String
}
pub trait Drawable {
fn draw(self) -> Boolean
}
pub struct Circle {
name: String,
radius: I32
}
// Declare conformance (fields are checked against struct definition)
impl Named for Circle {}
// Provide required methods
impl Drawable for Circle {
fn draw(self) -> Boolean {
self.radius > 0
}
}
Trait composition requires a separate impl block for each trait in the hierarchy:
pub trait Base {
fn id(self) -> I32
}
pub trait Extended: Base {
fn name(self) -> String
}
pub struct Item {
value: I32
}
impl Base for Item {
fn id(self) -> I32 {
self.value
}
}
impl Extended for Item {
fn name(self) -> String {
"item"
}
}
Conformance rules:
impl Trait for Typeis the only way to declare trait conformance- A struct or an enum can conform to a trait. Struct fields required
by the trait must be present in the struct definition
(
MissingTraitField,TraitFieldTypeMismatch). An enum has no fields, so it can only meet a trait with no field requirements - All
fnsignatures in the trait must be implemented in the impl block (MissingTraitMethod) - Method signatures must match exactly: the parameter count, each
label, each parameter type, the convention of
self, and the return type (TraitMethodSignatureMismatch) - Each trait in a chain needs its own impl block.
impl Extended for Itemdoes not satisfyBase: withoutimpl Base for Item, the error isMissingTraitMethod - An impl block provides only the methods of its own trait. Any other
method, also a method of a parent trait, is the error
MethodNotInTrait(E185). A method of the type alone goes in an impl block with no trait - A trait impl is part of its type. A module that imports the type
gets the impl too, with no
useof the trait. See Module System
Impls on Primitive Types
A primitive type (String, I32, I64, F32, F64, Boolean)
takes a regular impl, a trait impl and an extern impl. In a method
of a regular impl, self is a value of the primitive. A static method
is called on the type name: I32.zero(). An extern impl declares
methods that the host provides: see
Extern Declarations.
pub trait Zero {
fn zero() -> Self
}
impl I32 {
fn double(self) -> I32 { self * 2 }
}
impl Zero for I32 {
fn zero() -> Self { 0 }
}
fn origin<T: Zero>() -> T { T.zero() }
pub fn run_checks() {
assert(condition: 21.double() == 42)
assert(condition: I32.zero() == 0)
assert(condition: origin<I32>() == 0)
}
A method that the prelude declares on a primitive cannot be declared again with the same signature: see Extern Declarations.
Blanket Impls
A blanket impl is for each type that meets its bounds. impl<...>
declares its type parameters, and the target is one of them:
pub trait Named {
fn name(self) -> String
}
pub trait Greet {
fn greet(self) -> String
}
impl<T: Named> Greet for T {
fn greet(self) -> String { "hi " + self.name() }
}
pub struct Robot {
id: String
}
impl Named for Robot {
fn name(self) -> String { self.id }
}
fn welcome<X: Greet>(x: X) -> String { x.greet() }
pub fn run_checks() {
assert(condition: Robot(id: "r2").greet() == "hi r2")
assert(condition: welcome(x: Robot(id: "c3")) == "hi c3")
}
A parameter that only the trait arguments name takes its type from a
bound: in impl<U, T: Make<U>> MakeInto<T> for U, each type T with
impl Make<U> for T gives U the trait MakeInto<T>.
Rules:
- The compiler checks the body of a blanket impl once, as generic code. Then it writes a concrete impl for each primitive, and for each struct and enum with no type parameters, that meets the bounds
- Each parameter of
impl<...>must appear in the target or in the trait arguments: otherwise it is the errorUnconstrainedImplParameter(E182) - Only a trait impl can be a blanket impl:
impl<T> T { ... }is the errorInherentBlanketImpl(E181) - A blanket impl can be in an inline
mod. The compiler writes its concrete impls in that module, so the bodies name what the module sees. A blanket impl of the top level gives its trait to the types of each inline module too - A bound that needs the trait that the impl gives, such as
impl<T: Show> Show for T, is a cycle. A cycle proves nothing, so such an impl is for no type. A bound that asks about a larger trait argument at each step, such asT: A<[U]>in an impl ofA<U>forT, has no end: it is the errorBlanketRequirementTooDeep(E183) - A blanket impl gives its trait to a primitive, and to a struct or an
enum with no type parameters.
Box<I32>or[I32]does not get it impl<T: Show> Show for Box<T>is the other form ofimpl<...>: an impl of a generic type whose parameters have bounds- A blanket impl from another module can name only the public items of that module, because the concrete impl is written in the module that uses it
Two impls of one trait must not match one type. This is the error
OverlappingImpls (E180):
- Two blanket impls of one trait overlap when their trait arguments can be the same, whatever their bounds say. A type could meet both bounds, now or when a module adds an impl.
- A blanket impl and a concrete impl overlap when the type of the concrete impl meets the bounds of the blanket impl.
pub trait A { fn a(self) -> I32 }
pub trait B { fn b(self) -> I32 }
pub trait C { fn c(self) -> I32 }
impl<T: A> C for T { fn c(self) -> I32 { self.a() } }
impl<T: B> C for T { fn c(self) -> I32 { self.b() } } // error E180
An impl of a trait must be in the module of the trait or of the type: see Modules.
Conversions
The prelude declares four conversion traits:
| Trait | Method | Meaning |
|---|---|---|
From<T> | fn from(value: T) -> Self | makes a Self from a T |
Into<T> | fn into(self) -> T | makes a T from self |
TryFrom<T> | fn try_from(value: T) -> Self? | makes a Self from a T, or gives nil |
TryInto<T> | fn try_into(self) -> T? | makes a T from self, or gives nil |
Write From or TryFrom. The prelude gives Into and TryInto with
two blanket impls: impl<U, T: From<U>> Into<T> for U, and the same
for TryInto. A type with two From impls for one source gives that
source two into methods, and the type that the position expects
chooses one.
pub struct Meters {
v: I64
}
impl From<I64> for Meters {
fn from(value: I64) -> Self { Meters(v: value) }
}
pub struct Small {
v: I32
}
impl TryFrom<I32> for Small {
fn try_from(value: I32) -> Self? { if value < 10 { Small(v: value) } else { nil } }
}
pub fn run_checks() {
let m: Meters = 5I64.into()
assert(condition: m.v == 5I64)
assert(condition: Meters.from(value: 2I64).v == 2I64)
let s: Small? = 30.try_into()
assert(condition: s.is_none())
}
A conversion is explicit: write .into() or T.from(value: x). The
compiler converts in one place only: a value of T in a place that
expects T? wraps, with the prelude's From<T> for Optional<T>. So
Optional.from(value: 3) and 3 are the same I32?.
Operators
The prelude declares a trait for each arithmetic operator:
| Operator | Trait | Method |
|---|---|---|
+ | Add | fn add(self, other: Self) -> Self |
- | Sub | fn sub(self, other: Self) -> Self |
* | Mul | fn mul(self, other: Self) -> Self |
/ | Div | fn div(self, other: Self) -> Self |
% | Rem | fn rem(self, other: Self) -> Self |
On two operands of one struct or enum type that implements the trait,
the operator calls the method: a + b is a.add(other: b). A literal
operand takes the type of the other operand, or the type of the
position when both are literals. So let r: Rational = 1 / 3 divides
two Rational values, and the result is exactly 1/3.
The operators on the numbers stay built in. The prelude gives the
numbers these traits too, and String the trait Add, so a bound such
as T: Add takes them:
pub struct Money {
cents: I64
}
impl Add for Money {
fn add(self, other: Self) -> Self { Money(cents: self.cents + other.cents) }
}
fn total<T: Add>(a: T, b: T) -> T { a + b }
pub fn run_checks() {
let price = Money(cents: 150I64)
assert(condition: (price + price).cents == 300I64)
assert(condition: total(a: price, b: price).cents == 300I64)
assert(condition: total(a: 2, b: 3) == 5)
}
An operator on a type with no impl of its trait is the error
InvalidBinaryOp. Both operands must have the same type.
Trait-Bounded Polymorphism
A trait is a constraint, never the type of a value. Write
<T: Printable>, not item: Printable. Every call in FormaLang
dispatches statically, so there is no vtable and no indirect call.
pub trait Printable {
fn label(self) -> String
}
pub struct Doc {
text: String
}
impl Printable for Doc {
fn label(self) -> String { self.text }
}
// Correct: a generic bound. Monomorphisation makes one specialised
// function per concrete type, and the call is direct.
fn print_it<T: Printable>(item: T) -> String {
item.label()
}
// Error E063: a trait cannot be the type of a value.
fn print_any(item: Printable) -> String {
item.label()
}
The rule holds at every value position: a parameter, a return type, a
let annotation, a struct field, an enum variant field, an array
element, a dictionary value, and a closure parameter.
When the type is chosen at run time
A generic bound fixes one concrete type per call. For a value that may
hold any of several types, declare an enum with one variant per type
and match on it:
pub struct Square { side: I32 }
pub struct Rect { w: I32, h: I32 }
impl Square { fn area(self) -> I32 { self.side * self.side } }
impl Rect { fn area(self) -> I32 { self.w * self.h } }
pub enum AnyShape {
square(value: Square),
rect(value: Rect)
}
fn area_of(shape: AnyShape) -> I32 {
match shape {
.square(value): value.area(),
.rect(value): value.area()
}
}
// Two branches, one type — no cast needed.
fn pick(k: I32) -> AnyShape {
if k == 0 {
.square(value: Square(side: 2))
} else {
.rect(value: Rect(w: 2, h: 3))
}
}
The enum lists its variants in one place, so adding a type means
editing the enum and each match. In exchange the compiler checks that
every case is handled, and the generated code needs no vtable.
Generic Traits
Traits can themselves be generic, and constraints / impls can carry the concrete arguments:
pub trait Container<T> {
fn get(self) -> T
}
pub struct Box {
value: I32
}
impl Container<I32> for Box {
fn get(self) -> I32 { self.value }
}
fn unwrap<T: Container<I32>>(b: T) -> I32 {
b.get()
}
pub fn run_checks() {
assert(condition: unwrap(b: Box(value: 7)) == 7)
}
A bound on a generic trait gives its types. Through T: Container<I32>,
the method get returns an I32, and a parameter of type T in a
trait method takes an I32. A call through the bound is checked
against these types, and against the argument count and the labels of
the trait method. A generic trait needs its type arguments at each
bound and each impl: <T: Container> is the error
MissingGenericArguments, and a wrong count is the error
GenericArityMismatch.
One type can implement two instances of one generic trait. Each
instance is its own impl, and a bound says which one a call means. A
second impl of the same instance is a DuplicateDefinition. When both
instances answer a call, such as p.get() on a Pair below, the type
that the position expects chooses the instance whose method returns
it: let n: I32 = p.get(). With no expected type, the call is the
error AmbiguousCall: give the type, or make the call through a
bound.
pub trait Container<T> {
fn get(self) -> T
}
pub struct Pair {
n: I32,
s: String
}
impl Container<I32> for Pair {
fn get(self) -> I32 { self.n }
}
impl Container<String> for Pair {
fn get(self) -> String { self.s }
}
fn number<T: Container<I32>>(b: T) -> I32 { b.get() }
fn text<T: Container<String>>(b: T) -> String { b.get() }
pub fn run_checks() {
let p = Pair(n: 7, s: "seven")
assert(condition: number(b: p) == 7)
assert(condition: text(b: p) == "seven")
}
The monomorphisation pass clones generic traits, structs, enums,
and functions per unique argument tuple, then rewrites every
reference (including DispatchKind::Virtual on now-concrete
receivers) to point at the specialised clone. After mono runs, no
generic definitions remain in the IR.
A field of a generic trait takes the type arguments of the impl too:
in impl Coll<I32> for Bag, the field items: [T] of
trait Coll<T> is items: [I32]. Through a bound C: Coll<I32>, the
field c.items is an [I32].
Allowed trait positions:
- Generic constraint:
<T: Trait>or<T: Trait<X>> - Impl target:
impl Trait for Fooorimpl Trait<X> for Foo - Trait composition:
trait A: B + C
An impl is for every instance of a generic type. Its header names the
type parameters of the type: impl Sz for Box<T>. A header that names
a type there, such as impl Sz for Box<I32>, is the error
ImplForOneInstance (E178).
pub trait Sz { fn sz(self) -> I32 }
pub struct Box<T> { value: T }
impl Sz for Box<I32> { // error E178: I32 is a type, not a type parameter
fn sz(self) -> I32 { self.value }
}
The header uses the names that the type gives its type parameters, in
the same order. A header that renames a parameter, or puts the
parameters in another order, is the error
ImplRenamesTypeParameter (E184). The message names the parameters
of the type in their order.
pub struct Two<A, B> { first: A, second: B }
impl Two<B, A> { // error E184: write Two<A, B>
fn one(self) -> I32 { 1 }
}
A trait impl holds only the methods of its trait. A method that the
trait does not declare is the error MethodNotInTrait (E185). Put a
method of the type alone in an impl with no trait.
pub trait A { fn a(self) -> I32 }
pub struct P { n: I32 }
impl A for P {
fn a(self) -> I32 { 1 }
fn b(self) -> I32 { 2 } // error E185: A declares no method b
}
Rejected trait positions (use a generic bound instead):
- Function parameter type:
fn foo(x: Trait)✗ - Function return type:
fn make() -> Trait✗ - Let annotation:
let x: Trait = ...✗ - Struct/enum field:
field: Trait✗ - Closure params/return:
(x: Trait) -> I32✗