Type Classes¶
Type classes give Onion ad-hoc polymorphism: you can constrain a generic on the
operations a type must support, and the compiler supplies the right implementation
at each call. They are declared Rust-trait style, with trait, instance, and a
[T: Trait] context bound.
Declaring a trait¶
A trait describes operations over a type parameter:
A trait may have default methods, just like an interface:
Providing instances¶
An instance implements a trait for a concrete type:
trait Numeric[T] {
def zero(): T
def plus(a: T, b: T): T
}
instance Numeric[Integer] {
def zero(): Integer = 0
def plus(a: Integer, b: Integer): Integer = a + b
}
instance Numeric[Long] {
def zero(): Long = 0L
def plus(a: Long, b: Long): Long = a + b
}
There is at most one instance per (trait, type) (coherence). Primitive and
boxed forms are the same type, so Numeric[Int] and Numeric[Integer] are one
instance — declaring both is an error. The rule holds across the whole program: a
second instance Numeric[Int] in another module is the same error (E0097).
An instance is found when it is declared in the module of the code that needs it, in the
trait's module, in the type's module, or in a file with no module line.
Constrained generics¶
Write [T: Numeric] to require that T has a Numeric instance. Inside the body,
call the trait's methods through Numeric[T]::method(...):
trait Numeric[T] {
def zero(): T
def plus(a: T, b: T): T
}
instance Numeric[Integer] {
def zero(): Integer = 0
def plus(a: Integer, b: Integer): Integer = a + b
}
instance Numeric[Double] {
def zero(): Double = 0.0
def plus(a: Double, b: Double): Double = a + b
}
def sum[T: Numeric](xs: List[T]): T {
var acc: T = Numeric[T]::zero()
foreach x: T in xs { acc = Numeric[T]::plus(acc, x) }
return acc
}
def main(args: String[]): void {
println(sum([1, 2, 3, 4])) // => 10
println(sum([1.5, 2.5, 3.0])) // => 7.0
}
The compiler resolves the instance for the inferred T at each call and passes it
in for you — you never mention it. Calling a constrained function for a type with
no instance is a compile error, not a runtime failure.
You can combine a context bound with an extends upper bound and use several,
[T extends Comparable[T]: Numeric] or [T: Numeric, U: Numeric].
Ground dictionary access¶
Trait[ConcreteType]::method(...) works directly, outside any generic:
trait Numeric[T] {
def plus(a: T, b: T): T
}
instance Numeric[Integer] {
def plus(a: Integer, b: Integer): Integer = a + b
}
def main(args: String[]): void {
println(Numeric[Integer]::plus(3, 4)) // => 7
}
Constrained functions calling constrained functions¶
A constrained function can call another with the same abstract type parameter; the instance is forwarded automatically:
trait Numeric[T] {
def zero(): T
def plus(a: T, b: T): T
}
instance Numeric[Integer] {
def zero(): Integer = 0
def plus(a: Integer, b: Integer): Integer = a + b
}
def sum[T: Numeric](xs: List[T]): T {
var acc: T = Numeric[T]::zero()
foreach x: T in xs { acc = Numeric[T]::plus(acc, x) }
return acc
}
def sumTwice[T: Numeric](xs: List[T]): T = Numeric[T]::plus(sum(xs), sum(xs))
def main(args: String[]): void {
println(sumTwice([1, 2, 3])) // => 12
}
Your own traits¶
Type classes are not limited to numbers — define any trait you need:
trait Eq[T] {
def eq(a: T, b: T): Boolean
}
instance Eq[Integer] {
def eq(a: Integer, b: Integer): Boolean = a == b
}
def allSame[T: Eq](xs: List[T]): Boolean {
if xs.size() < 2 { return true }
val head: T = xs.get(0)
foreach x: T in xs {
if !Eq[T]::eq(head, x) { return false }
}
return true
}
def main(args: String[]): void {
println(allSame([7, 7, 7])) // => true
println(allSame([7, 8, 7])) // => false
}
Current limitations¶
The first release focuses on the common cases:
- Single-parameter traits (
trait C[T]). - Context bounds on functions and methods (
def f[T: C]); a bound on a generic class (class Box[T: C]) parses but is not enforced yet. - Trait methods are called explicitly via
Trait[T]::method(...); UFCSvalue.method(...)is not yet available. - Instances are for ground types (
instance Numeric[Integer]), not parameterized (instance Numeric[List[T]]). - There is no built-in
Numeric/Eq/Ordyet — define the traits you need.