Skip to content

Overview

Often when writing code you want to create similar classes or functions that differ only in the type that they operate on. The classic example of this is collection classes. You want to be able to create an Array that can hold objects of a particular type without creating an IntArray, StringArray, etc. This is where generics step in.

Generic Classes

A generic class is a class that can have parameters, much like a method has parameters. The parameters for a generic class are types, including their reference capability. Parameters are introduced to a class using square brackets.

Take the following example of a non-generic class:

class Foo
  var _c: U32

  new create(c: U32) =>
    _c = c

  fun get(): U32 => _c

  fun ref set(c: U32) => _c = c

actor Main
  new create(env:Env) =>
    let a = Foo(42)
    env.out.print(a.get().string())
    a.set(21)
    env.out.print(a.get().string())

This class only works for the type U32, a 32 bit unsigned integer. We can make this work over other types by making the type a parameter to the class. For this example it looks like:

class Foo[A: Any val]
  var _c: A

  new create(c: A) =>
    _c = c

  fun get(): A => _c

  fun ref set(c: A) => _c = c

actor Main
  new create(env:Env) =>
    let a = Foo[U32](42)
    env.out.print(a.get().string())
    a.set(21)

    env.out.print(a.get().string())
    let b = Foo[F32](1.5)
    env.out.print(b.get().string())

    let c = Foo[String]("Hello")
    env.out.print(c.get().string())

The first thing to note here is that the Foo class now takes a type parameter in square brackets, [A: Any val]. That syntax for the type parameter is:

Name: Constraint ReferenceCapability

In this case, the name is A, the constraint is Any and the reference capability is val. Any is used to mean that the type can be any type - it is not constrained. The remainder of the class definition replaces U32 with the type name A.

The user of the class must provide a type when referencing the class name. This is done when creating it:

let a = Foo[U32](42)
let b = Foo[F32](1.5)
let c = Foo[String]("Hello")

That tells the compiler what specific class to create, replacing A with the type provided. For example, a Foo[String] usage becomes equivalent to:

class FooString
  var _c: String val

  new create(c: String val) =>
    _c = c

  fun get(): String val => _c

  fun ref set(c: String val) => _c = c

Type parameter defaults

Sometimes the same parameter type is used over and over again, and it is tedious to always specify it when using the generic class. The class Bar expects its type parameter to be a USize val by default:

class Bar[A: Any box = USize val]
  var _c: A

  new create(c: A) =>
    _c = c

  fun get(): A => _c

  fun ref set(c: A) => _c = c

Now, when the default type parameter is the desired one, it can simply be omitted. But it is still possible to be explicit or use a different type.

let a = Bar(42)
let b = Bar[USize](42)
let c = Bar[F32](1.5)

Note that we could simply write class Bar[A: Any box = USize] because val is the default reference capability of the USize type.

Generic Methods

Methods can be generic too. They are defined in the same way as normal methods but have type parameters inside square brackets after the method name:

primitive Foo
  fun bar[A: Stringable val](a: A): String =>
    a.string()

actor Main
  new create(env:Env) =>
    let a = Foo.bar[U32](10)
    env.out.print(a.string())

    let b = Foo.bar[String]("Hello")
    env.out.print(b.string())

This example shows a constraint other than Any. The Stringable type is any type with a string() method to convert to a String.

These examples show the basic idea behind generics and how to use them. Real world usage gets quite a bit more complex and the following sections will dive deeper into how to use them.