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.
8.1 Defining Traits
Section titled “8.1 Defining Traits”Basic Trait Definition
Section titled “Basic Trait Definition”A trait declares a set of methods that types must implement:
trait Printable = function format(self): StringTraits can have multiple methods:
trait Describable = function describe(self): String function summary(self): StringTraits with Parameters
Section titled “Traits with Parameters”Trait methods can take parameters:
trait Comparable = function compare(self, other: Self): OrderingThe Self type refers to the implementing type.
Generic Traits
Section titled “Generic Traits”Traits can have type parameters:
trait Container<T> = function get(self): T function set(self, value: T): Self8.2 Implementing Traits
Section titled “8.2 Implementing Traits”Basic Implementation
Section titled “Basic Implementation”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)"Implementing Multiple Traits
Section titled “Implementing Multiple Traits”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"Implementing Traits for Enums
Section titled “Implementing Traits for Enums”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"Implementations and Private Types
Section titled “Implementations and Private Types”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.
Implementing Generic Traits
Section titled “Implementing Generic Traits”trait Container<T> = function get(self): T
record Box<T> = value: T
implement <T> Container<T> for Box<T> = function get(self): T = self.valueTrait Properties
Section titled “Trait Properties”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 getterKey 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
Implementing Trait Properties
Section titled “Implementing Trait Properties”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 == 20Trait properties can reference each other in implementations, just like class body properties can reference other fields.
Combining Trait Methods and Properties
Section titled “Combining Trait Methods and Properties”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})"Using Trait Methods
Section titled “Using Trait Methods”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"Trait Bounds in Functions
Section titled “Trait Bounds in Functions”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())Automatic Import of Implementations
Section titled “Automatic Import of Implementations”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 codeimport users.User
// All implementations from the users package come with Userlet u1 = User { name = "Alice"; email = "alice@example.com" }let u2 = User { name = "Bob"; email = "bob@example.com" }u1 == u2 // Equatable worksu1 < u2 // Comparable worksu1.toJson() // JsonSerializable worksImporting 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 codeimport serialization.JsonWriter
// All implementations from the serialization package come with JsonWriter// You can now use JsonWriter methods on FileOutput, StringBuffer, HttpResponse8.3 Core Traits
Section titled “8.3 Core Traits”Dovetail provides several built-in traits that enable common operations.
Equatable
Section titled “Equatable”Enables == and != comparisons:
trait Equatable = function equals(self, other: Self): BoolPrimitive 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 fieldslet p1 = Point { x = 1; y = 2 }let p2 = Point { x = 1; y = 2 }p1 == p2 // trueIdentity Equality and Hashing
Section titled “Identity Equality and Hashing”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 identityThe 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.
Indexed reads and writes
Section titled “Indexed reads and writes”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] = itemSlice<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): OutputThe 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 + rightThe 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.
Unary negation
Section titled “Unary negation”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 = -valueBigInt and Decimal implement Neg. Primitive signed integers and floats
retain their native unary negation; this trait does not add implicit conversions.
Concat
Section titled “Concat”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 +.
Comparable
Section titled “Comparable”Enables ordering comparisons (<, >, <=, >=):
trait Comparable = function compare(self, other: Self): Ordering // Less, Equal, or GreaterNumeric 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) // 208.4 The Orphan Rule
Section titled “8.4 The Orphan Rule”The orphan rule prevents conflicts when multiple packages could implement the same trait for the same type.
The Rule
Section titled “The Rule”You can only implement a trait for a type if:
- You own the trait (it’s defined in your package), OR
- You own the type (it’s defined in your package)
Why This Matters
Section titled “Why This Matters”Without this rule, two different packages could implement the same trait for the same type differently, causing conflicts:
// Package Aimplement 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!Valid Implementations
Section titled “Valid Implementations”// In your package: you own the traittrait MyTrait = function doSomething(self): Int32
// OK - you own the traitimplement MyTrait for Int32 = function doSomething(self): Int32 = self * 2// In your package: you own the typerecord MyRecord = value: Int32
// OK - you own the typeimplement Printable for MyRecord = function format(self): String = "${self.value}"Invalid Implementation
Section titled “Invalid Implementation”// Trying to implement someone else's trait for someone else's type// ERROR: Orphan rule violationimplement SomeoneElsesTrait for SomeoneElsesType = function method(self): Int32 = 0Working Around the Orphan Rule
Section titled “Working Around the Orphan Rule”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 itnewtype MyWrapper = ThirdPartyType
implement ThirdPartyTrait for MyWrapper = function method(self): Int32 = 428.5 Trait and Interface Inheritance
Section titled “8.5 Trait and Interface Inheritance”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.
8.6 Default Methods and Properties
Section titled “8.6 Default Methods and Properties”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:
- Members of the receiver type’s associated module.
- Members of imported named extensions.
- 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.
Summary
Section titled “Summary”-
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 = expressionto satisfy trait property requirements -
Automatic Imports: Importing a type brings its trait implementations; importing a trait brings all its implementations
-
Core Traits:
Equatablefor==,Comparablefor 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:
extendsincludes 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
