Skip to content

Traits and Implementations

Traits define shared behavior that types can implement and generic functions can require through bounds. When a contract must also be a value type, use an interface. Traits and interfaces share implementation blocks, inheritance, defaults, and explicit member qualification.


A trait declares a set of methods that types must implement:

trait Printable =
function format(self): String

Traits can have multiple methods:

trait Describable =
function describe(self): String
function summary(self): String

Trait methods can take parameters:

trait Comparable =
function compare(self, other: Self): Ordering

The Self type refers to the implementing type.

Traits can have type parameters:

trait Container<T> =
function get(self): T
function set(self, value: T): Self

Use implement to make a type conform to a trait:

record Point =
x: Int32
y: Int32
trait Printable =
function format(self): String
implement Printable for Point =
function format(self): String =
"(${self.x}, ${self.y})"
let p = Point { x = 3; y = 4 }
p.format() // "(3, 4)"

A type can implement multiple traits:

trait Printable =
function format(self): String
trait Describable =
function describe(self): String
record User =
name: String
age: Int32
implement Printable for User =
function format(self): String = self.name
implement Describable for User =
function describe(self): String =
"${self.name} is ${self.age} years old"
enum Color =
Red
Green
Blue
trait Printable =
function format(self): String
implement Printable for Color =
function format(self): String =
match self with
case Red => "red"
case Green => "green"
case Blue => "blue"

An implement block does not gain access to a type’s private operations. For private records and enums, it may read fields and match patterns, but must call associated-module functions to construct values or update a record with with. A private newtype also requires module functions to inspect its wrapped value:

newtype Amount private = Int32
module Amount =
public function make(value: Int32): Amount =
assert value >= 0
Amount(value)
public function read(self): Int32 = self.value
trait Counted =
function count(self): Int32
implement Counted for Amount =
public function count(self): Int32 = self.read()

Writing self.value or an Amount(value) pattern directly in that implementation is an error. The rule also applies to generic implementations, extensions, and generated implementations; sharing the defining package does not grant access.

trait Container<T> =
function get(self): T
record Box<T> =
value: T
implement <T> Container<T> for Box<T> =
function get(self): T = self.value

Traits can declare properties that types must provide. Properties in traits define getters that implementations must satisfy:

trait Measurable =
property size(self): Int32
record Box =
width: Int32
height: Int32
depth: Int32
implement Measurable for Box =
property size(self): Int32 = self.width * self.height * self.depth
function main(): Unit =
let b = Box { width = 2; height = 3; depth = 4 }
assert b.size == 24 // calls the property getter

Key points about trait properties:

  • Declared with property name(self): Type; an optional body provides a default
  • Implementations without a default provide the property using property name(self): Type = expression
  • Properties are accessed like fields but call getter functions
  • Properties are evaluated every time they’re accessed

When implementing a trait with properties, provide the property implementation(self):

trait Stats =
property average(self): Int32
property total(self): Int32
record DataSet =
values: (Int32, Int32, Int32)
implement Stats for DataSet =
property total(self): Int32 =
self.values._0 + self.values._1 + self.values._2
property average(self): Int32 =
self.total / 3
function main(): Unit =
let data = DataSet { values = (10, 20, 30) }
assert data.total == 60
assert data.average == 20

Trait properties can reference each other in implementations, just like class body properties can reference other fields.

Traits can have both methods and properties:

trait Shape =
property area(self): Int32
property perimeter(self): Int32
function describe(self): String
record Rectangle =
width: Int32
height: Int32
implement Shape for Rectangle =
property area(self): Int32 = self.width * self.height
property perimeter(self): Int32 = 2 * (self.width + self.height)
function describe(self): String = "Rectangle(${self.width}x${self.height})"

Once a trait is implemented, you can call its methods:

let user = User { name = "Alice"; age = 30 }
debug(user.format()) // "Alice"
debug(user.describe()) // "Alice is 30 years old"

Use traits as constraints in generic functions:

function printAll<T>(items: Array<T>): Unit
where T: Printable =
for item in items do
debug(item.format())

Trait implementations are automatically imported along with types and traits. You never need to explicitly import implementations.

Importing a type brings its implementations:

When you import a type, all trait implementations for that type (defined in its package) come with it.

// In package "users"
record User =
name: String
email: String
implement Equatable for User = ...
implement Comparable for User = ...
implement JsonSerializable for User = ...
// In your code
import users.User
// All implementations from the users package come with User
let u1 = User { name = "Alice"; email = "alice@example.com" }
let u2 = User { name = "Bob"; email = "bob@example.com" }
u1 == u2 // Equatable works
u1 < u2 // Comparable works
u1.toJson() // JsonSerializable works

Importing a trait brings its implementations:

When you import a trait, all implementations of that trait (defined in its package) come with it.

// In package "serialization"
trait JsonWriter =
function writeJson(self, value: Any): String
implement JsonWriter for FileOutput = ...
implement JsonWriter for StringBuffer = ...
implement JsonWriter for HttpResponse = ...
// In your code
import serialization.JsonWriter
// All implementations from the serialization package come with JsonWriter
// You can now use JsonWriter methods on FileOutput, StringBuffer, HttpResponse

Dovetail provides several built-in traits that enable common operations.

Enables == and != comparisons:

trait Equatable =
function equals(self, other: Self): Bool

Primitive types have prelude implementations of Equatable. Records and enums can request structural equality with @derive(Equatable) when their fields/variants are equatable.

@derive(Equatable)
record Point =
x: Int32
y: Int32
// Derived equality compares the Int32 fields
let p1 = Point { x = 1; y = 2 }
let p2 = Point { x = 1; y = 2 }
p1 == p2 // true

ClassIdentity uses reference identity, not the contents of an object. Two separately created instances of the same class have different identities, even when all their fields contain equal values. Two references to the same instance share its identity.

Classes can implement equality and hashing explicitly using reference identity:

class IdentityBox<T>(public value: T)
implement <T> Equatable for IdentityBox<T> =
public function equals(self, other: IdentityBox<T>): Bool =
ClassIdentity.equals(self, other)
implement <T> Hashable for IdentityBox<T> =
public function hash(self): Int64 = ClassIdentity.hash(self)
function checkBoxIdentity(): Unit =
let first = IdentityBox(42)
let second = IdentityBox(42)
let alias = first
assert !ClassIdentity.equals(first, second) // Same contents, different instances
assert ClassIdentity.equals(first, alias) // Two references to one instance
assert first != second // The Equatable implementation above uses identity

The element type needs no equality or hashing bounds: these implementations use the box’s identity. Neither implementation is automatic, and an implementation for a base class does not automatically provide one for every subclass.

Hashable.hash returns an Int64. Whenever two values compare equal through Equatable, their trait hashes must match. Collisions between unequal values are permitted. A class using structural equality should therefore hash its equality-relevant contents; using identity hashing for structurally equal but distinct objects would generally violate the contract.

Raw ClassIdentity.equals and ClassIdentity.hash remain available even if a class defines structural trait implementations. They do not call those implementations. The current mutable hash collections also require Default for keys; providing these two traits alone does not remove that separate requirement. A class can implement Default using a shared placeholder instance when that is safe for its API. For example, the runtime’s Waiter uses one global instance with an empty suspended-fiber stack for unused hash-table slots, while real scheduler conditions always allocate fresh instances. The collections track occupied slots separately and do not mutate this placeholder’s suspended-fiber stack.

Index<K> enables value[key] on user types. Its associated Output is the read result. IndexSet<K> independently enables value[key] = item; its associated Value is the accepted item type. Keys need not be integers.

function read<C, K, T>(collection: C, key: K): T where C: Index<K, Output = T> = collection[key]
function write<C, K, T>(collection: C, key: K, item: T): Unit where C: IndexSet<K, Value = T> = collection[key] = item

Slice<T> implements both capabilities with Int32 keys. ReadonlySlice<T> implements only reads. A method named get or set does not grant the corresponding syntax without the trait. Array and String indexing retain their built-in behavior; they do not implement these traits. Range slicing ([|start..end|]) is a separate operation.

Implementations are selected by receiver and compatible key parameter type, using ordinary argument assignability. Neither the expected read result nor the assigned value resolves ambiguous implementations. Receiver, key, and value are evaluated once, in that order.

Arithmetic operators and associated output constraints

Section titled “Arithmetic operators and associated output constraints”

Add<R>, Sub<R>, Mul<R>, and Div<R> overload +, -, *, and /. Each declares an associated Output and a method named add, sub, mul, or div:

public trait Add<R> =
type Output
function add(self: Self, rhs: R): Output

The operands select the implementation, whose Output determines the result. Numbers implement these traits with native arithmetic; Uint128 supports addition, subtraction, and multiplication, but not division. Strings concatenate with ++.

Generic functions constrain associated outputs using equality bindings in bounds:

function add<L, R, O>(left: L, right: R): O where L: Add<R, Output = O> =
left + right

The compiler can infer O from the matching implementation. A function returning its left operand’s type instead uses where L: Add<R, Output = L>; Output = Self is never assumed. The same bindings work for ordinary trait methods and for associated types nested inside other types. Positional trait arguments precede bindings. A helper can also name an implementation’s output as T.Output. See Associated Outputs for a complete example. Equality bindings for generic associated types remain unsupported.

Newtypes must implement each capability explicitly, including Equatable and Comparable. Wrapping a number or String does not grant its operators. Public newtypes may opt into structural equality with @derive(Equatable). Private newtypes delegate through their associated modules to preserve access rules.

Neg lets library types overload unary - without changing their result type:

public trait Neg =
function negate(self: Self): Self
function opposite<T>(value: T): T where T: Neg = -value

BigInt and Decimal implement Neg. Primitive signed integers and floats retain their native unary negation; this trait does not add implicit conversions.

Overloads ++, the concatenation operator:

trait Concat<R> =
type Output
function concat(self: Self, rhs: R): Output

++ is deliberately separate from +: + is arithmetic, while ++ is “join these two things”. Implementing Concat does not overload +, and vice versa.

String implements Concat<String> with Output = String, backed by native concatenation. Other types opt in:

record Line =
text: String
implement Concat<Line> for Line =
type Output = Line
function concat(self: Line, other: Line): Line =
Line { text = self.text ++ other.text }
let joined = Line { text = "a" } ++ Line { text = "b" }

Like Div, the right-hand type is a parameter and Output is an associated type, so the operands need not match and the result need not be Self:

implement Concat<String> for Line =
type Output = Line
function concat(self: Line, other: String): Line = Line { text = self.text ++ other }

++ is left-associative and binds at the same level as +.

Enables ordering comparisons (<, >, <=, >=):

trait Comparable =
function compare(self, other: Self): Ordering // Less, Equal, or Greater

Numeric types and strings implement Comparable by default.

function max<T>(a: T, b: T): T
where T: Comparable =
if a > b then a else b
max(10, 20) // 20

The orphan rule prevents conflicts when multiple packages could implement the same trait for the same type.

You can only implement a trait for a type if:

  1. You own the trait (it’s defined in your package), OR
  2. You own the type (it’s defined in your package)

Without this rule, two different packages could implement the same trait for the same type differently, causing conflicts:

// Package A
implement Printable for Int32 =
function format(self): String = "number: $self"
// Package B (different implementation!)
implement Printable for Int32 =
function format(self): String = "int($self)"
// Which one should be used? Conflict!
// In your package: you own the trait
trait MyTrait =
function doSomething(self): Int32
// OK - you own the trait
implement MyTrait for Int32 =
function doSomething(self): Int32 = self * 2
// In your package: you own the type
record MyRecord =
value: Int32
// OK - you own the type
implement Printable for MyRecord =
function format(self): String = "${self.value}"
// Trying to implement someone else's trait for someone else's type
// ERROR: Orphan rule violation
implement SomeoneElsesTrait for SomeoneElsesType =
function method(self): Int32 = 0

If you need to implement a foreign trait for a foreign type, wrap it in a newtype:

// Can't implement ThirdPartyTrait for ThirdPartyType directly
// Solution: wrap it
newtype MyWrapper = ThirdPartyType
implement ThirdPartyTrait for MyWrapper =
function method(self): Int32 = 42

extends includes a parent’s members in a contract. A trait can extend traits or interfaces; an interface can extend only interfaces.

interface Named =
function name(self): String
interface Described extends Named =
function description(self): String
record Item =
title: String
implement Described for Item =
function name(self): String = self.title
function description(self): String = "Item: ${self.name()}"
function main(): Unit =
let item: Described = Item { title = "Book" }
let named: Named = item
assert named.name() == "Book"
assert item.description() == "Item: Book"

The child implementation supplies inherited requirements in the same block, unless a default body supplies them. A separate implementation of the parent does not fill missing members in the child implementation.

Implementing the child also makes the concrete type satisfy the parent. If the type has a direct parent implementation, that implementation wins when the parent contract is requested. Otherwise, one child implementation can supply the parent; multiple possible providers are ambiguous. Inherited methods can share a name when their parameter lists differ. See Inherited Overloads for an example.

A trait or interface member can provide a body. An implementation may omit that member or replace its body. Defaults can call other members of the contract:

trait Measured =
property size(self): Int32
property doubled(self): Int32 = self.size * 2
function describe(self): String = "Size: ${self.size}"
record Batch =
count: Int32
implement Measured for Batch =
property size(self): Int32 = self.count
function main(): Unit =
let batch = Batch { count = 3 }
assert batch.doubled == 6
assert batch.describe() == "Size: 3"

A child contract can override an inherited default by providing a new body. Repeating only the inherited signature is not an override. Conflicting defaults from parents require an explicit implementation to resolve the conflict.

Defaults work in implementation blocks and classes, including generic ones. Methods with their own type parameters can also provide defaults; see Generic Trait Defaults. Classes must implement required static trait members explicitly.

8.7 Method Resolution and Explicit Disambiguation

Section titled “8.7 Method Resolution and Explicit Disambiguation”

For a concrete receiver, applicable members are considered in this order:

  1. Members of the receiver type’s associated module.
  2. Members of imported named extensions.
  3. Trait or interface implementations.

Named extensions must be imported even within the same package. Multiple applicable candidates at the same priority are ambiguous. Qualify a member to choose its contract or extension explicitly:

trait ShortLabel =
function label(self): String
trait LongLabel =
function label(self): String
record Entry =
title: String
implement ShortLabel for Entry =
function label(self): String = self.title
implement LongLabel for Entry =
function label(self): String = "Entry: ${self.title}"
function main(): Unit =
let entry = Entry { title = "Book" }
assert ShortLabel.label(entry) == "Book"
assert LongLabel.label(entry) == "Entry: Book"

Use Trait<Arguments>.method(receiver, arguments) for a generic contract, or ExtensionName.method(receiver, arguments) for a named extension. Properties also support qualification as a call with the receiver argument.

Implementations must not overlap: two implementations that could apply to the same type and contract are rejected. Different where bounds do not establish that implementations are disjoint. A blanket implementation whose target is a bare type parameter, such as implement <T> Printable for T, is unsupported.


  • Traits define shared behavior as a set of methods and properties

  • Trait Properties: Declared with property name(self): Type, implementations provide the getter

  • Implementations make types conform to traits using implement Trait for Type

  • Implementing Properties: Use property name(self): Type = expression to satisfy trait property requirements

  • Automatic Imports: Importing a type brings its trait implementations; importing a trait brings all its implementations

  • Core Traits: Equatable for ==, Comparable for ordering

  • Orphan Rule: You must own either the trait or the type to implement a trait

  • Workaround: Use newtypes to wrap foreign types when needed

  • Inheritance: extends includes parent requirements; child implementations supply inherited members or use defaults

  • Defaults: Methods and properties can supply reusable bodies

  • Disambiguation: TraitName.method(receiver) selects a contract explicitly