ferrule

traits and implementations

α2
impl-blocksautomatic-resolutionderiveeffect-aware-traits

traits and implementations

ferrule has no trait keyword. traits are record types. impl registers canonical implementations. the compiler resolves automatically when unambiguous.

why no trait keyword

record types already describe interfaces:

type Eq<T> = { eq: (T, T) -> Bool }

adding a trait keyword would create two ways to say the same thing. records-as-traits are simpler, composable with &, and work with ferrule's existing type system.

defining interfaces

type Eq<T> = { eq: (T, T) -> Bool }
type Hash<T> = { hash: (T) -> u64 }
type Show<T> = { show: (T) -> String }
type Ord<T> = { compare: (T, T) -> Ordering }

these are just record types. nothing special about them.

implementing interfaces with impl

impl Eq<UserId> {
    eq: function(a: UserId, b: UserId) -> Bool { return a.id == b.id },
}

impl Hash<UserId> {
    hash: function(u: UserId) -> u64 { return u.id },
}

impl Show<UserId> {
    show: function(u: UserId) -> String { return fmt"User({u.id})" },
}

impl registers the canonical implementation of a record type for a given type. under the hood, impl Eq<UserId> { ... } creates const UserId.Eq: Eq<UserId> = { ... }.

automatic resolution in generic functions

function dedupe<T>(items: View<T>) -> Vec<T>
    where T impl Eq & Hash
    effects [alloc]
{
    // compiler resolves T.Eq and T.Hash automatically
}

// clean usage:
const unique = dedupe(users)

the where T impl Eq & Hash constraint means: T must have impl Eq<T> and impl Hash<T> registered.

explicit override when needed

const unique = dedupe(users, with Hash = caseInsensitiveHash)

when the automatic resolution isn't what you want, pass an explicit implementation.

derive generates impl blocks

type Point = derive(Eq, Hash, Show) {
    x: f64,
    y: f64,
}
// generates: impl Eq<Point>, impl Hash<Point>, impl Show<Point>

derive uses comptime introspection to generate implementations.

combining interfaces

use intersection types:

type Hashable<T> = Eq<T> & Hash<T>
type HashShow<T> = Hashable<T> & Show<T>

effect-aware traits

trait methods can declare effects:

type Serialize<T> = {
    serialize: (T, Writer) -> Unit effects [fail<EncodeError>],
}

type Deserialize<T> = {
    deserialize: (Reader) -> T effects [alloc, fail<DecodeError>],
}

impl Serialize<User> {
    serialize: function(u: User, w: Writer) -> Unit effects [fail<EncodeError>] {
        check w.write_string(u.name)
        check w.write_u32(u.age)
    },
}

no other systems language has effect-tracked traits.

no orphan rules

you can impl anything for anything in your own module. if two modules provide conflicting impls for the same type + record, it's a compile error at the use site. you must disambiguate.

explicit passing still works

for cases where you want multiple implementations or don't want automatic resolution:

function dedupe<T>(items: View<T>, h: Hashable<T>) -> Vec<T> effects [alloc] {
    // explicit, like the old way
}
const unique = dedupe(users, UserId.hashable)

both styles coexist. impl + automatic resolution is sugar, not a replacement.

comparison to other languages

languageapproachferrule difference
rusttrait keyword + impl blocksno separate trait keyword, records ARE traits
gostructural interfacesferrule is nominal, requires explicit impl
haskelltype classessimilar concept, but no orphan rules, effect-aware
zigno traits, explicit passingferrule adds impl sugar on top of explicit passing

On this page