Skip to content

Traits

A trait is a special type of record that can be used to define a set of functions that must be implemented for a given type.

Syntax of Traits

Trait declarations

Traits are declared using the trait keyword, followed by a record type declaration.

trait
type <record name> <type parameters> :=
    | <type-constructor1> {
        <field1-1> : <type-1-1>;
        ...
        <field1-n> : <type-1-n>
        }
    | ...
    | <type-constructor-n> {
        <fieldn-1> : <type-n-1>;
        ...
        <fieldn-n> : <type-n-n>
    };

For example, the following defines a trait Show. Any type A that implements Show must provide a function show that takes an A and returns a String.

trait
type Show A := mkShow {show : A  String};

Instance declarations

An instance of a trait refers to a term of the corresponding record type, which must implement all methods stipulated by the trait.

Syntax of instance declarations

To declare a given term is an instance of a trait, we use the instance keyword. To define an instance of a trait, we use the trait constructor of the corresponding record type.

instance
<term> : {{<trait>}} := <trait constructor> (
    field1 := <term1>;
    ...
    fieldn := <termn>;
);

For example, we could define three instances of Show for String, Bool, and Nat as follows:

instance
showStringI : Show String := mkShow (show := id);

instance
showBoolI : Show Bool := mkShow (show := λ {x := if x "true" "false"});

instance
showNatI : Show Nat := mkShow (show := natToString);

One more involved example is the following, which defines an instance of Show for the type of lists:

instance
showListI {A} {{Show A}} : Show (List A) :=
  let
    showList {A} : {{Show A}}  List A  String
      | nil := "nil"
      | (h :: t) := Show.show h ++str " :: " ++str showList t;
  in mkShow (show := showList);

Usage

Using the Show trait defined above, we can define a function showNat that takes a Nat and returns a String. One possible implementation is the following:

module usage-example;
  type Nat :=
    | Z
    | S Nat;

  trait
  type Show A := mkShow {show : A  String};
  
  NatToString : Nat -> String
    | Z := "Z"
    | (S n) := "S " ++str NatToString n;

  instance
  showNat : Show Nat := mkShow (show := NatToString);
end;

To prevent looping during instance search, we ensure a structural decrease in the trait parameters of instance types. Therefore, the following is rejected:

type Box A := box A;

trait
type T A := mkT {pp : A  A};

We check whether each parameter is a strict subterm of some trait parameter in the target. This ordering is included in the finite multiset extension of the subterm ordering, hence terminating.

  • Matching on implicit instances

It is possible to manually provide an instance and to match on implicit instances, as shown below:

f {A} {{Show A}} : A  String
  | x := Show.show x;

f' {A} : {{Show A}}  A  String
  | {{mkShow s}} x := s x;

f'' {A} : {{Show A}}  A  String
  | {{M}} x := Show.show {{M}} x;

Finally, using the Show trait and the function printStringLn and IO from the standard library, we could use the instances of Show as follows:

main : IO :=
  printStringLn (Show.show true)
    >> printStringLn (f false)
    >> printStringLn (Show.show 3)
    >> printStringLn (Show.show [true; false])
    >> printStringLn (Show.show [1; 2; 3])
    >> printStringLn (f' [1; 2])
    >> printStringLn (f'' [true; false])
    >> printStringLn (Show.show ["a"; "b"; "c"; "d"]);