Traits¶
A trait is a compile-time contract: a named set of functions over a type,
each taking that type as self. A type satisfies a trait through one
implementation, impl Trait for Type. A function of a trait is either
required, a signature with no body that every implementation supplies, or
has a default body, which an implementation inherits unless it writes the
function itself.
A trait and an implementation¶
Shape requires area and gives describe a default body. impl Shape for
Rect writes area alone:
trait Shape {
fn area(self) : Float
fn describe(self) : String = fmt("area {}", self.area())
}
type Rect { width : Float, height : Float }
impl Shape for Rect {
fn area(self) : Float = self.width * self.height
}
describe is a function of every Rect all the same, with the trait's body.
The programs below run with these declarations before them:
Rect(width = 2.0f, height = 3.0f).describe()
"area 6.0"
A default body calls the implementation¶
Inside a default body, self.area() resolves to the area of the type
self has. The body is written once, against the required functions, and
each type it runs over contributes its own:
type Circle { radius : Float }
impl Shape for Circle {
fn area(self) : Float = 3.0f * self.radius * self.radius
}
[Rect(width = 2.0f, height = 3.0f).describe(), Circle(radius = 1.0f).describe()]
["area 6.0", "area 3.0"]
Replacing a default¶
An implementation that writes a function the trait gives a default replaces the default for its type, and leaves it in place for every other type:
type Square { side : Float }
impl Shape for Square {
fn area(self) : Float = self.side * self.side
fn describe(self) : String = fmt("a square of side {}", self.side)
}
[Rect(width = 2.0f, height = 3.0f).describe(), Square(side = 2.0f).describe()]
["area 6.0", "a square of side 2.0"]
What an implementation must supply¶
An implementation without one of the required functions is refused against
its impl line, and the diagnostic names the function:
trait Shape {
fn area(self) : Float
fn describe(self) : String = fmt("area {}", self.area())
}
type Circle { radius : Float }
impl Shape for Circle {
fn describe(self) : String = "a circle"
}
Circle(radius = 1.0f).describe()
type error: `impl Shape for Circle` lacks `area`
--> <expression>:8:1
|
8 | impl Shape for Circle {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A trait has one implementation per type in a program. A second is refused:
trait Named {
fn name(self) : String
}
type Dog { tag : String }
impl Named for Dog {
fn name(self) : String = self.tag
}
impl Named for Dog {
fn name(self) : String = "a dog"
}
Dog(tag = "rex").name()
type error: `Named` is already implemented for Dog, in this program, at byte 71: a trait has one implementation per type
--> <expression>:11:1
|
11 | impl Named for Dog {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Generic functions over a trait¶
A bound, T : Shape, restricts a type parameter to the types that
implement the trait. Inside the body x.area() resolves through the bound,
and every call is checked against it:
type Circle { radius : Float }
impl Shape for Circle {
fn area(self) : Float = 3.0f * self.radius * self.radius
}
fn total<T : Shape>(xs: List<T>) : Float = xs | map(x => x.area()) | sum
[total([Rect(width = 2.0f, height = 3.0f), Rect(width = 1.0f, height = 1.0f)]), total([Circle(radius = 1.0f)])]
[7.0, 3.0]
A type with no implementation is refused at the call:
trait Shape {
fn area(self) : Float
}
type Line { length : Float }
fn total<T : Shape>(xs: List<T>) : Float = xs | map(x => x.area()) | sum
total([Line(length = 2.0f)])
type error: `total` needs T : Shape, and Line does not implement `Shape`
--> <expression>:9:1
|
9 | total([Line(length = 2.0f)])
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
A trait is not a type¶
A trait's name appears only where a contract is meant: in a bound, in an
impl, and in another trait's declaration. It is not a type, so no
parameter, binding or list is typed by one:
trait Shape {
fn area(self) : Float
}
fn doubled(s: Shape) : Float = s.area() * 2.0f
1
name error: unknown type `Shape`
--> <expression>:5:15
|
5 | fn doubled(s: Shape) : Float = s.area() * 2.0f
| ^^^^^
A list that holds a Rect and a Circle together is typed by a union of the
two, and a match reaches each member at its own type, where its
implementation applies:
type Figure = union { Rect, Circle }
figures : List<Figure> = [Rect(width = 2.0f, height = 3.0f), Circle(radius = 1.0f)]
figures | map(f => match f {
Rect r => r.describe(),
Circle c => c.describe(),
})
["area 6.0", "area 3.0"]
A later release names that union in the trait's declaration, trait Shape as
Figure, so that List<Figure> holds every implementing type and
f.describe() needs no match. The current release does not parse it yet.
A trait extending a trait¶
A trait may extend others, trait Greeter : Named. The functions of the
traits it extends are its own, so its default bodies may call them, and an
implementation of it supplies them:
trait Named {
fn name(self) : String
}
trait Greeter : Named {
fn greet(self) : String = fmt("hello, {}", self.name())
}
type Dog { tag : String }
impl Greeter for Dog {
fn name(self) : String = self.tag
}
Dog(tag = "rex").greet()
"hello, rex"
Implementing Greeter does not implement Named. A bound T : Named takes
only the types with an impl Named of their own:
trait Named {
fn name(self) : String
}
trait Greeter : Named { }
type Dog { tag : String }
impl Greeter for Dog {
fn name(self) : String = self.tag
}
fn names<T : Named>(xs: List<T>) : List<String> = xs | map(x => x.name())
names([Dog(tag = "rex")])
type error: `names` needs T : Named, and Dog does not implement `Named`
--> <expression>:15:1
|
15 | names([Dog(tag = "rex")])
| ^^^^^^^^^^^^^^^^^^^^^^^^^
Operators are traits¶
The operators resolve through the core traits, which are in scope
everywhere: + through Add, - through Sub, * through Mul, /
through Div, % through Rem, == through Eq, xs[i] through Index
and xs[a:b] through Slice. Implementing one gives a type its operator:
type Vec2 { x : Int, y : Int }
impl Add for Vec2 {
fn add(self, other: Vec2) : Vec2 = Vec2(x = self.x + other.x, y = self.y + other.y)
}
Vec2(x = 1, y = 2) + Vec2(x = 3, y = 4)
Vec2(x = 4, y = 6)
A type implementing Ord is ordered. Its compare decides <, sort, min
and max:
type Version { major : Int, minor : Int }
impl Ord for Version {
fn compare(self, other: Version) : Ordering =
compare(self.major * 100 + self.minor, other.major * 100 + other.minor)
}
[Version(major = 1, minor = 10), Version(major = 1, minor = 2)] | sort | map(v => v.minor)
[2, 10]
Traits and inheritance¶
A default body is the part of class inheritance a trait has: code written once against functions each type defines. The rest is absent.
| Class inheritance | A trait |
|---|---|
| a subclass inherits method bodies | an implementation inherits default bodies |
| a subclass inherits fields | a trait declares no fields |
| a subclass is a subtype of its parent | a trait is not a type; no value is held as a Shape |
a method overrides along a chain of parents, reaching up with super |
an implementation replaces a default, one level, with no super |
| a class extends one parent or several | a trait extends traits; a type implements each trait it needs, once |
Where one value is genuinely another, as a Card is a Doc, HQL says so
with an ordinary : between types, not with a trait.
A receiver function without a trait¶
A receiver module adds functions to a type with no trait at all.
module counting for Int makes each self function after it a function of
every Int:
module counting for Int
fn twice(self) : Int = self * 2
[3.twice(), 21.twice()]
[6, 42]
The functions reach only the source unit that declares the module or imports
it. Elsewhere the call is a name error, as "hi".shout() is without
import mighty.text (Modules):
"hi".shout()
name error: String has no function `shout`; did you mean `sort`?
--> <expression>:1:1
|
1 | "hi".shout()
| ^^^^^^^^^^^^
Two imported modules that declare one name over one type make a call by that name ambiguous, and the call is refused until it is qualified. A declaration the unit writes itself wins over every import's.
A trait is the form for a function that generic code quantifies over or an operator resolves through. A receiver module is the form for a function a type gains that nothing quantifies over.
A module that implements a trait¶
A later release joins the two forms: a receiver module lists traits after its pattern, and its
self functions are the implementation of those traits. The current release
does not parse the trait list yet:
module shapes.circle for Circle : Shape
type Circle { radius : Float }
fn area(self) : Float = 3.0f * self.radius * self.radius
module shapes.circle for Circle : Shape means impl Shape for Circle with
the module's functions as the body: describe comes from the trait's
default, and Circle(radius = 1.0f).describe() is "area 3.0".