Traits
A trait names an ability that several types can have, like "can be compared" or "can be printed". It's how one function can work on many types while still using something specific about each one.
Declaring a trait
Traits go in the traits zone. A trait has a type
parameter and a list of operations:
traits
Area<a> {
area(shape: a) -> Float
}
This says: a type a has an Area if there's
an area function that takes an a and
returns a Float.
Implementing a trait
Implementations go in the impls zone, near the end of
the file:
impls
Area for Shape {
area(shape) {
match shape {
Circle(r) -> 3.14 * r * r,
Square(s) -> s * s,
}
}
}
The parameter and return types come from the trait, so you don't
repeat them. Once the implementation exists,
area(Circle(1.0)) works like any other function call.
Generic functions that need a trait
A generic function can't assume anything about its type variable. To
use a trait's operations on it, ask for the trait with
where:
bigger(a: a, b: a) -> a where Area<a> {
if area(a) >= area(b) { a } else { b }
}
bigger works for any type that has an
Area. Calling it with a type that doesn't is a compile
error, which names the missing implementation.
The standard library uses this a lot. For example,
Map.get needs to compare keys, so its signature says
where Eq<k>.
Deriving common traits
Writing implementations for equality or printing by hand gets
repetitive. derive(…) after a type asks Polar to write
them for you:
types
Shape = Circle(Float) | Square(Float) derive(Eq, Show)
Point = { x: Int, y: Int } derive(Eq, Show)
| Trait | Gives you |
|---|---|
Eq |
== and !=, comparing field by
field
|
Show |
Text for #{…} interpolation, like
Circle(1) or { x: 1, y: 2 }
|
Json |
Encoding and decoding with Std.Json. See
Working with JSON
|
Without Show, a type can't go inside #{…}:
error[POLAR0707]: `Secret` has no `Show` impl
--> noshow.px:10:24
|
10 | Log.info("value: #{s}")
| ^ this needs `Show`
That's sometimes exactly what you want. A type holding a password probably shouldn't be easy to print.
Eq and Show are always there
Eq and Show come from the prelude, which
every module sees without importing it. The basic types already
implement both. To call show yourself, either write
Prelude.show(x), or bring the method into scope:
uses
Std.Prelude { show }
The braces after a module name import trait methods, so you can call them without the module prefix.
All together
module Shapes
traits
Area<a> {
area(shape: a) -> Float
}
types
Shape = Circle(Float) | Square(Float) derive(Eq, Show)
Point = { x: Int, y: Int } derive(Eq, Show)
functions
bigger(a: a, b: a) -> a where Area<a> {
if area(a) >= area(b) { a } else { b }
}
main() {
let p: Point = { x: 1, y: 2 }
let q: Point = { x: 1, y: 2 }
Log.info("#{Circle(1.0)} and #{Square(2.0)}")
Log.info("#{p}")
Log.info("same point? #{p == q}")
Log.info("same shape? #{Circle(1.0) == Square(1.0)}")
Log.info("bigger: #{bigger(Circle(1.0), Square(2.0))}")
}
impls
Area for Shape {
area(shape) {
match shape {
Circle(r) -> 3.14 * r * r,
Square(s) -> s * s,
}
}
}
exports
main
$ polar run shapes.px
Circle(1) and Square(2)
{ x: 1, y: 2 }
same point? true
same shape? false
bigger: Square(2)
So far, every function we've written has been pure: it only computes a value. Next, we'll look at what happens when code needs to touch the outside world, with effects.