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 fn are structural requirements (the struct must have them)
  • fn signatures 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:

  • self must be the first parameter. In another position it is a ParseError.
  • A method without a self parameter is a static method. A read of self in it is the error UndefinedReference.
  • A free function has no receiver, so a self parameter there is a ParseError.
  • self is 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 when Name is 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 takes self needs 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 error UndefinedReference. Write Counter.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 on Box<I32>. A call that gets no type for a parameter of the struct is the error MissingGenericArguments
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, Self is the target type. In impl Box<T>, Self is Box<T>. Self(value: v) and Self.zero() name the target type too.
  • In a trait, Self is the type that implements the trait. An impl gives Self its own type: in impl Zero for Point, the trait's fn zero() -> Self is fn zero() -> Point.
  • In other places, Self is the error UndefinedType.

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 Type is 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 fn signatures 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 Item does not satisfy Base: without impl Base for Item, the error is MissingTraitMethod
  • 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 use of 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 error UnconstrainedImplParameter (E182)
  • Only a trait impl can be a blanket impl: impl<T> T { ... } is the error InherentBlanketImpl (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 as T: A<[U]> in an impl of A<U> for T, has no end: it is the error BlanketRequirementTooDeep (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 of impl<...>: 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:

TraitMethodMeaning
From<T>fn from(value: T) -> Selfmakes a Self from a T
Into<T>fn into(self) -> Tmakes 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:

OperatorTraitMethod
+Addfn add(self, other: Self) -> Self
-Subfn sub(self, other: Self) -> Self
*Mulfn mul(self, other: Self) -> Self
/Divfn div(self, other: Self) -> Self
%Remfn 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 Foo or impl 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 ✗