Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Trait Implementations and Coherence

Ferlium lets user code implement both standard-library traits and user-defined traits. This chapter covers the surface syntax for impl, generic impls, explicit trait bindings, and the rules that keep trait resolution unambiguous.

Basic impl syntax

For a simple single-input trait with no explicit output types, the syntax is:

struct Counter(int)

impl SizedSeq for Counter {
    fn len(counter: Counter) {
        counter.0
    }
}

This is the compact form of an impl header. It works well for traits that conceptually operate on one main type. You can think of it as sugar for an explicit binding such as impl SizedSeq for <Self = Counter> { ... }.

Generic impls

Impls can introduce their own generic parameters with Rust-like binder syntax:

struct Bag<T>([T])

impl<T> SizedSeq for Bag<T> {
    fn len(bag: Bag<T>) {
        len(bag.0)
    }
}

Here the impl says: for every T, Bag<T> is a sized sequence. The same binder syntax also works for more involved generic impls, including iterator-like types. The next sections show those richer impl headers.

Impl-level where clauses

Impls may also carry their own where clause. This is useful when an implementation only applies under extra trait assumptions:

struct Wrapper<T>(T)

impl<T> SizedSeq for Wrapper<T>
where
    T: Iterator<Item = int>
{
    fn len(wrapper: Wrapper<T>) {
        count(wrapper.0)
    }
}

The extra constraints become part of the impl itself. They participate in trait selection and in coherence checking, just like the types named in the impl header.

Explicit bindings

Some traits relate more than one input type, or also expose output slots. In those cases, an impl header can spell the bindings explicitly.

Multi-input traits

For multi-parameter traits such as Cast, named bindings are usually the clearest choice:

struct Wrapper<T>(T)

impl<T> Cast for <From = T, To = Wrapper<T>> {
    fn cast(value: T) -> Wrapper<T> {
        Wrapper(value)
    }
}

Ferlium also accepts positional input bindings such as impl<T> Cast for <T, Wrapper<T>> { ... }. Named bindings are usually easier to read, and they line up naturally with traits that also have output slots.

Traits with output types

Traits such as Iterator have output slots. You may write them explicitly:

struct TransformIter<I, T, O>
where
    I: Iterator<Item = T>
{
    iterator: I,
    mapper: (T) -> O,
}

impl<I, T, O> Iterator for <Self = TransformIter<I, T, O> |-> Item = O> {
    fn next(it: &mut TransformIter<I, T, O>) -> None | Some(O) {
        match next(it.iterator) {
            Some(value) => Some(it.mapper(value)),
            None => None,
        }
    }
}

Writing output bindings is optional. If you omit them, Ferlium infers them from the method signatures. If you write them explicitly, they must agree with the inferred ones.

Coherence

Ferlium uses a strict coherence rule for trait implementations:

  • for any given trait application, there must be at most one applicable impl
  • overlapping impls are rejected

This keeps trait resolution predictable. The compiler never has to choose arbitrarily between two equally valid impls.

For example, this is rejected because both impls target the same trait and type:

struct Wrapper(int)

impl Serialize for Wrapper {
    fn serialize(value: Wrapper) {
        None
    }
}

impl Serialize for Wrapper {
    fn serialize(value: Wrapper) {
        None
    }
}

The same rule also rejects overlapping generic impls. For example, if two generic impl declarations could both apply to the same types, Ferlium rejects them.

The orphan rule

Ferlium does not let an arbitrary module implement an existing foreign trait for arbitrary foreign input types. In practice, when you implement an existing trait, at least one input type in the impl must be a local named type that belongs to your module.

This restriction is called the orphan rule. More generally:

  • a local trait may be implemented freely
  • a foreign trait requires at least one local named input type in the impl

This is allowed, because Counter is local to the current module:

struct Counter(int)

impl SizedSeq for Counter {
    fn len(counter: Counter) {
        counter.0
    }
}

This is rejected, because both the trait and the input type are foreign:

impl SizedSeq for int {
    fn len(value: int) {
        1
    }
}

The orphan rule prevents unrelated modules from attaching competing impls to the same foreign types. Combined with coherence, it keeps trait resolution predictable across module boundaries, which is important for separate compilation.

Current scope

Today, user code can:

  • implement standard-library and user-defined traits
  • write generic impls
  • write impl-level where clauses
  • use explicit trait input and output bindings in impl headers

User code still cannot:

  • write per-method generic parameter lists inside traits or impls
  • write method-local where clauses inside trait impls

What comes next

The next chapter moves into the standard library and shows how sequence-processing functions such as map, filter, collect, and common reductions work over collections and iterators.