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.