polar

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.