Interfaces
Interfaces (typeclasses) provide ad-hoc polymorphism — the ability to write code that works for any type that satisfies a contract, without inheritance.
Defining an Interface
interface Show(a) do
fn show : a -> String
end
An interface declares one or more function signatures that implementing types must provide.
Default Implementations
Interfaces can provide default implementations that types inherit unless overridden:
interface Eq(a) do
fn eq : a -> a -> Bool
fn neq : a -> a -> Bool do
fn x y -> !eq(x, y)
end
end
Any type implementing Eq automatically gets neq for free. It only needs to implement eq.
Superinterfaces
interface Ord(a) requires Eq(a) do
fn cmp : a -> a -> Int
fn lt : a -> a -> Bool do fn x y -> cmp(x, y) < 0 end
fn gt : a -> a -> Bool do fn x y -> cmp(x, y) > 0 end
fn le : a -> a -> Bool do fn x y -> cmp(x, y) <= 0 end
fn ge : a -> a -> Bool do fn x y -> cmp(x, y) >= 0 end
end
Ord(a) requires Eq(a) means any type implementing Ord must also implement Eq.
Implementing an Interface
Use impl Interface(Type) do ... end:
type Color = Red | Green | Blue
impl Show(Color) do
fn show(c) do
match c do
Red -> "Red"
Green -> "Green"
Blue -> "Blue"
end
end
end
impl Eq(Color) do
fn eq(a, b) do
match (a, b) do
(Red, Red) -> true
(Green, Green) -> true
(Blue, Blue) -> true
_ -> false
end
end
end
Now you can call show(Red) or eq(Red, Blue) and the dispatch is resolved by the type.
Conditional Implementations
Implement an interface for a generic type with constraints:
-- Show for List(a) when a has Show
impl Show(List(a)) when Show(a) do
fn show(xs) do
let items = List.map(xs, fn x -> show(x))
"[" ++ String.join(items, ", ") ++ "]"
end
end
-- Eq for List(a) when a has Eq
impl Eq(List(a)) when Eq(a) do
fn eq(xs, ys) do
match (xs, ys) do
(Nil, Nil) -> true
(Cons(x, xt), Cons(y, yt)) -> eq(x, y) && eq(xt, yt)
_ -> false
end
end
end
The compiler picks the right implementation at call sites based on the concrete type.
Using Interfaces in Function Signatures
Constrain type parameters with when:
fn print_all(xs : List(a)) : () when Show(a) do
List.iter(xs, fn x -> println(show(x)))
end
fn sort(xs : List(a)) : List(a) when Ord(a) do
-- implementation uses lt/gt from Ord
Sort.timsort(xs)
end
fn unique(xs : List(a)) : List(a) when Eq(a) do
List.dedup(xs)
end
Multiple constraints:
fn sort_and_show(xs : List(a)) : String when Ord(a), Show(a) do
let sorted = sort(xs)
show(sorted)
end
Standard Interfaces
Eq(a) — Equality
interface Eq(a) do
fn eq : a -> a -> Bool
fn neq : a -> a -> Bool do fn x y -> !eq(x, y) end
end
Usage:
eq(42, 42) -- true
eq("hi", "bye") -- false
neq(1, 2) -- true
Ord(a) — Ordering
interface Ord(a) requires Eq(a) do
fn cmp : a -> a -> Int -- negative = less, 0 = equal, positive = greater
fn lt : a -> a -> Bool
fn gt : a -> a -> Bool
fn le : a -> a -> Bool
fn ge : a -> a -> Bool
end
Usage:
cmp(1, 2) -- -1
cmp(2, 2) -- 0
cmp(3, 2) -- 1
lt(1, 2) -- true
Show(a) — String Representation
interface Show(a) do
fn show : a -> String
end
Usage:
show(42) -- "42"
show(true) -- "true"
show([1,2,3]) -- "[1, 2, 3]" (if List has Show)
Hash(a) — Hashing
interface Hash(a) do
fn hash : a -> Int
end
Required for keys in Map and elements in Set.
derive — Automatic Implementations
For types with straightforward structure, derive generates implementations automatically:
type Point = { x : Float, y : Float }
derive Eq, Show for Point
type Status = Active | Inactive | Suspended
derive Eq, Ord, Show, Hash for Status
After derive Eq for Point, you can use eq on Point values.
derive works for:
Eq— structural equality, comparing all fields/constructorsOrd— lexicographic ordering by fields, constructor order for variantsShow— pretty-printed representationHash— consistent hash based on structure
type User = { name : String, age : Int, role : String }
derive Eq, Ord, Show, Hash for User
let u1 = { name = "Alice", age = 30, role = "admin" }
let u2 = { name = "Bob", age = 25, role = "user" }
show(u1) -- "{ name = \"Alice\", age = 30, role = \"admin\" }"
eq(u1, u1) -- true
lt(u1, u2) -- depends on lexicographic field order
Multiple types in one derive:
derive Json, Eq for MyType
derive Show for Color
A Complete Example: Implementing a Container
mod MyStack do
type Stack(a) = Stack(List(a))
fn empty() : Stack(a) do Stack(Nil) end
fn push(Stack(xs), x) do Stack(Cons(x, xs)) end
fn pop(Stack(xs)) : Option((a, Stack(a))) do
match xs do
Nil -> None
Cons(h, t) -> Some((h, Stack(t)))
end
end
fn size(Stack(xs)) : Int do List.length(xs) end
impl Show(Stack(a)) when Show(a) do
fn show(Stack(xs)) do
let items = List.map(xs, fn x -> show(x))
"Stack[" ++ String.join(items, ", ") ++ "]"
end
end
impl Eq(Stack(a)) when Eq(a) do
fn eq(Stack(xs), Stack(ys)) do eq(xs, ys) end
end
end
-- Using the stack:
fn main() do
let s0 = MyStack.empty()
let s1 = MyStack.push(s0, 1)
let s2 = MyStack.push(s1, 2)
let s3 = MyStack.push(s2, 3)
println(show(s3)) -- "Stack[3, 2, 1]"
match MyStack.pop(s3) do
Some((top, rest)) ->
println("popped: " ++ int_to_string(top))
println("remaining: " ++ show(rest))
None ->
println("empty stack")
end
end
Interface Dispatch
The compiler resolves interface dispatch at compile time (after monomorphization). There are no vtables or runtime type lookups — interface calls are direct function calls to the concrete implementation.
This means:
- Zero overhead compared to a direct call
- The compiler can inline implementations across interface boundaries
- No boxing required for primitive types
Next Steps
- Types — types you implement interfaces for
- Standard Library — stdlib types and their interface implementations
- Pattern Matching — using
matchwith interface-dispatched values