Skip to main content

Generic Type Definitions

Generic types are parameterized type definitions. Instead of writing separate aliases for Box<Int>, Box<Bool>, and Box<Text>, you define one shape Box<T> and instantiate it with concrete types. The same monomorphization model used for generic functions applies to generic type definitions during verification.

Basic Syntax

Type parameters appear in angle brackets after the type name:

type Box<T> = { value : T };

The <T> declares a type parameter. Inside the type definition, T acts as a placeholder for any concrete type. When you use the type, provide a concrete type argument.

Multiple Type Parameters

Types can have multiple parameters separated by commas:

type Pair<A, B> = (A, B);
type Result<T, E> = { #ok : T; #err : E };
type Triple<X, Y, Z> = (X, Y, Z);

Use descriptive names when the meaning is clear: <Key, Value> for maps, <Ok, Err> for results.

Common Generic Type Patterns

generic-type-basic.sr9
// Basic generic type definitions

module GenericTypes {
// Single type parameter
public type Box<T> = { value : T };

// Multiple type parameters
public type Pair<A, B> = (A, B);

// Generic variant (tagged union)
public type Result<T, E> = { #ok : T; #err : E };

// Generic option wrapper
public type Option<T> = ?T;

// Recursive generic type
public type List<T> = ?(T, List<T>);
};

persistent actor {
public func useBox() : async Nat
ensures result == 42;
{
let b : GenericTypes.Box<Nat> = { value = 42 };
b.value
};

public func usePair() : async Int
ensures result == 10;
{
let p : GenericTypes.Pair<Int, Bool> = (10, true);
p.0
};

public func useResult() : async Nat
ensures result == 100;
{
let r : GenericTypes.Result<Nat, Text> = #ok(100);
switch r {
case (#ok(n)) n;
case (#err(_)) 0;
}
};
}

These patterns appear throughout Sector9 code:

PatternDefinitionPurpose
Box<T>{ value : T }Wrap a single value
Pair<A, B>(A, B)Two values together
Option<T>?TOptional value
Result<T, E>{ #ok : T; #err : E }Success or failure
List<T>?(T, List<T>)Linked list

Instantiating Generic Types

Use a generic type by providing type arguments in angle brackets:

let box : Box<Int> = { value = 42 };
let pair : Pair<Text, Bool> = ("hello", true);
let result : Result<Nat, Text> = #ok(100);

Type arguments replace type parameters throughout the definition. Box<Int> becomes { value : Int }.

Type Inference

The compiler can infer type arguments from context:

// Explicit: type arguments provided
let b1 : Box<Int> = { value = 42 };

// Inferred: type determined from usage
let b2 = { value = 42 }; // Underlying record type inferred

Aliases are transparent, so inference usually finds the underlying structural type rather than a nominal alias name. Provide explicit type arguments when:

  • The type cannot be inferred
  • You want to be explicit for readability
  • Multiple valid types could apply

Bounded Type Parameters

Constrain type parameters with upper bounds using <::

generic-type-bounds.sr9
// Generic types with bounded type parameters

module Bounded {
// T must have a 'name' field
public type Named<T <: { name : Text }> = {
item : T;
count : Nat
};

// T must have an 'id' field
public type Identified<T <: { id : Nat }> = {
data : T;
timestamp : Int
};
};

persistent actor {
type Person = { name : Text; age : Nat };
type Product = { id : Nat; price : Nat };

public func testNamed() : async Text
ensures result == "Alice";
{
let p : Person = { name = "Alice"; age = 30 };
let named : Bounded.Named<Person> = { item = p; count = 1 };
named.item.name
};

public func testIdentified() : async Nat
ensures result == 42;
{
let prod : Product = { id = 42; price = 100 };
let entry : Bounded.Identified<Product> = { data = prod; timestamp = 0 };
entry.data.id
};
}

The bound T <: { name : Text } restricts T to types with at least a name field. Any type satisfying the constraint can be used.

Bound Syntax

// Single bound
type Named<T <: { name : Text }> = { item : T };

// Multiple parameters with bounds
type Keyed<K <: { id : Nat }, V> = Map<K, V>;

// Default bound (implicit)
type Any<T> = { data : T }; // T <: Any (implicit)

Without an explicit bound, type parameters default to Any, accepting any type.

Recursive Generic Types

Generic types can reference themselves to create recursive data structures:

generic-type-recursive.sr9
// Recursive generic type definitions

module Recursive {
// Linked list
public type List<T> = ?(T, List<T>);

// Binary tree
public type Tree<T> = {
value : T;
left : ?Tree<T>;
right : ?Tree<T>
};

// Helper to count list elements
public pure func length<T>(list : List<T>) : Nat {
switch list {
case null 0;
case (?(_, tail)) 1 + length<T>(tail);
}
};
};

persistent actor {
public func testList() : async Nat
ensures result == 3;
{
// Build list: 1 -> 2 -> 3 -> null
let list : Recursive.List<Nat> = ?(1, ?(2, ?(3, null)));
Recursive.length<Nat>(list)
};

public func testTree() : async Int
ensures result == 10;
{
let tree : Recursive.Tree<Int> = {
value = 10;
left = null;
right = null
};
tree.value
};
}

Valid Recursive Patterns

// Linked list: base case is null
type List<T> = ?(T, List<T>);

// Binary tree: base case is null children
type Tree<T> = { value : T; left : ?Tree<T>; right : ?Tree<T> };

// Variant with recursive case
type Expr<T> = { #lit : T; #add : (Expr<T>, Expr<T>) };

Recursive types must have a valid base case. The type must be "productive" - not infinitely expanding.

Generic Types with Spec Collections

Ghost state often uses generic spec collections:

generic-type-spec.sr9
// Generic types with spec collections

persistent actor {
// Ghost state using generic spec collections
ghost var balances : Map<Nat, Nat> = Map.empty<Nat, Nat>();
ghost var _allowed : Set<Nat> = Set.empty<Nat>();
ghost var log : Seq<Int> = Seq.empty<Int>();

invariant Map.card(balances) >= 0;
invariant Seq.len(log) >= 0;

public func deposit(user : Nat, amount : Nat) : async ()
modifies balances
ensures Map.contains(user, balances);
{
ghost {
let current = Map.getOr(balances, user, 0);
balances := Map.update(balances, user, current + amount);
};
};

public func record(value : Int) : async ()
modifies log
ensures Seq.len(log) == old(Seq.len(log)) + 1;
{
ghost {
log := Seq.concat(log, Seq.singleton(value));
};
};
}

The built-in spec collections are all generic:

CollectionTypePurpose
Map<K, V>Key-value pairsAssociative mapping
Set<T>Unique elementsMembership tracking
Seq<T>Ordered elementsIndexed access
Multiset<T>Elements with countsBag semantics

Element Type Restrictions

Spec collection elements, keys, and values must be spec-immutable, except opaque/token handles by identity. These types are rejected:

// WRONG: mutable records
type Cell = { var x : Int };
ghost let cells : Set<Cell> = Set.empty(); // Error M0242

// WRONG: mutable arrays
ghost let arrs : Seq<[var Int]> = Seq.empty(); // Error M0242

// WRONG: function types
ghost let fns : Set<Nat -> Nat> = Set.empty(); // Error M0242

// WRONG: actor types
ghost let actors : Map<actor {}, Nat> = Map.empty(); // Error M0242

Valid element types include primitives, immutable records, tuples, options, and variants with immutable payloads. See collection-linearity restrictions for the verifier rule behind this.

Variance

Generic types have variance that determines subtyping behavior:

PositionVarianceSubtyping Rule
Output (return)CovariantBox<Sub> <: Box<Super>
Input (parameter)ContravariantHandler<Super> <: Handler<Sub>
BothInvariantExact match required

Variance in Practice

// Covariant: option element
?Sub <: ?Super // when Sub <: Super

// Covariant: variant payloads
{ #ok : Sub } <: { #ok : Super }

// Invariant: mutable containers
[var Sub] is NOT a subtype of [var Super]

See Subtyping Rules for complete variance rules.

Verification and Monomorphization

Sector9 uses monomorphization for verification. Each generic type used with different type arguments creates a specialized version internally.

When you write:

type Box<T> = { value : T };
let intBox : Box<Int> = { value = 42 };
let boolBox : Box<Bool> = { value = true };

The verifier treats Box<Int> and Box<Bool> as distinct types. Each instantiation is verified with its concrete types.

Viper Output

In generated Viper code (--viper), generic types appear as specialized definitions:

// Box<Int> becomes a distinct ADT
adt Box$Int { ... }

// Box<Bool> becomes another ADT
adt Box$Bool { ... }

Type arguments are encoded in the generated names.

Restrictions

Several patterns are rejected for generic type definitions.

Non-Productive Types

Types must have a valid base case:

// Rejected: no base case
type Bad<T> = Bad<T>;

// Rejected: circular without termination
type A<T> = B<T>;
type B<T> = A<T>;

Expansive Types

Type parameters must appear as immediate arguments, not wrapped:

// T is immediate argument
type Good<T> = ?Good<T>;

// T in tuple, recursion immediate
type List<T> = ?(T, List<T>);

// T in variant payload
type Tree<T> = { #leaf : T; #node : (Tree<T>, Tree<T>) };

Cyclic Bounds

Type parameter bounds cannot form cycles:

// Rejected: self-referential bound
func f<A <: A>(a : A) { };

// Rejected: mutual cycle
func g<A <: B, B <: A>(a : A, b : B) { };

Outer Scope Capture

Types cannot capture parameters from enclosing scopes:

// Rejected: D references outer parameter A
class C<A>() {
type D = A; // Error M0137
};

Error Reference

Error CodeCondition
M0137Type references outer scope parameter
M0043Cyclic type bounds
M0044Duplicate type parameter names
M0045Wrong number of type arguments
M0046Type argument violates bound
M0242Mutable type in spec collection

Best Practices

  1. Use generics for reusable abstractions: type Result<T, E> is better than separate IntResult, BoolResult, etc.

  2. Prefer explicit type arguments: When in doubt, write Box<Int> rather than relying on inference

  3. Name parameters meaningfully: <Key, Value> is clearer than <K, V> for complex types

  4. Document bounds: When a type parameter has constraints, explain why in comments

  5. Keep recursion simple: Prefer direct recursion like List<T> over complex mutual recursion

Summary

  • Define generic types with type Name<T> = ...
  • Multiple parameters: type Pair<A, B> = (A, B)
  • Instantiate with type arguments: Box<Int>
  • Bounds constrain parameters: <T <: Constraint>
  • Recursive types need valid base cases
  • Spec collection elements, keys, and values must be immutable
  • The verifier specializes generic uses: each instantiation is verified with its concrete type arguments
  • Non-productive, expansive, and cyclic definitions are rejected