Skip to main content

Generics

Generics let you prove things once, and use the result for many different types.

Several different Acorn keywords can be used in a generic way, by adding a list of type parameters.

Generics with structure

You can define a generic structure by putting a list of generic parameters after the type name.

For example, let's define a LatticePoint that doesn't just have to be composed of integers, but can be any type.

structure LatticePoint[T] {
x: T
y: T
}

Here, T is the generic parameter.

If we define LatticePoint this way, then LatticePoint[Int] works exactly the same as if we had defined it with:

structure LatticePoint {
x: Int
y: Int
}

You can have multiple generic parameters in a single parameter list.

structure Pair[T, U] {
first: T
second: U
}

Generics with inductive

Similarly, you can define a generic inductive type.

For example, let's define the generic List type.

inductive List[T] {
nil
cons(T, List[T])
}

A list can be a list of anything, but a particular list can only be a list of one type of thing. A List[Int] is a list of Int, a List[Nat] is a list of Nat, a List[List[Bool]] is a list of List[Bool], and so on.

Generics with attributes

You can define attributes for a parametrized type by adding parameters to the attributes statement. The parameters have to match those used in the initial definition of the type.

For example:

attributes List[T] {
// Whether this list contains a particular item.
define contains(self, item: T) -> Bool {
match self {
List.nil {
false
}
List.cons(head, tail) {
if head = item {
true
} else {
tail.contains(item)
}
}
}
}
}

Acorn tries to infer the type for generics, so that you don't have to include the [T] everywhere. In this example, we only have to use the type parameter T to specify the type of the item argument.

Generics with define

You can define a generic function by including type parameters after the function name.

define colinear[T](a: LatticePoint[T], b: LatticePoint[T]) -> Bool {
a.x = b.x or a.y = b.y
}

Similarly, you can define member functions on a generic structure.

attributes LatticePoint[T] {
define swap(self) -> LatticePoint[T] {
LatticePoint.new(self.y, self.x)
}
}

When you call generic functions, you generally do not need to provide the type parameters, because they can be inferred from the types of the arguments.

import Int
numerals Int

// No parameters on LatticePoint.new are needed.
let origin: LatticePoint[Int] = LatticePoint.new(0, 0)

// No parameters on colinear are needed.
theorem origin_self_colinear {
colinear(origin, origin)
}

Generics with theorem

Similarly, you can define generic theorems by including type parameters after the theorem name.

theorem swap_is_involutive[T](p: LatticePoint[T]) {
p.swap.swap = p
} by {
p.swap.x = p.y
p.swap.y = p.x
}

When you prove a generic theorem, it can be instantiated to any type.

Generics with let

If I told you that the let statement supported generics, and I didn't give you any sample code, I bet you could guess how it worked anyway.

But just in case. It looks like this:

let is_finite[T]: Bool = exists(xs: List[T]) {
forall(x: T) {
xs.contains(x)
}
}

The generic let statement defines a constant for each type. You can think of it as a property of the type itself. In this case, it's defining whether the type itself is finite.