Inductive Type with Indices
Warning
This feature is not yet implemented or not fully supported in the current version of Saki interpreter/REPL.
In type theory, inductive types are fundamental constructs that allow the definition of complex data structures by specifying their constructors. In previous sections, we introduced algebraic data types (ADTs) as a special case of inductive types. An extension of inductive types involves introducing indices, allowing the type to vary depending on certain parameters. This leads to the concept of indexed families or inductive types with indices. This feature enable the encoding of rich invariants directly into the type system.
Syntax of Inductive Types
The general form for defining inductive types in Saki is:
InductiveConsType ::= InductiveConsTypeAtom (‘->’ InductiveConsTypeAtom)*
InductiveConsTypeAtom ::= ( Term | (‘this’ ImplicitArgList? ExplicitArgList?) )
InductiveCons ::= Ident ‘:’ InductiveConsType
InductiveTypeTerm ::= ‘inductive’ ExplicitArgList? ‘{’ InductiveCons (NL+ InductiveCons)* ‘}’
The syntax includes:
- InductiveConsType: Specifies the type of a constructor. It consists of zero or more parameter types, followed by a return type, which can be a
Term
or the inductive type itself, denoted bythis
. - InductiveCons: Declares a constructor with an identifier (
Ident
), a colon, and its type (InductiveConsType
). - InductiveTypeTerm: Defines the inductive type, starting with the
inductive
keyword, optionally followed by explicit arguments (ExplicitArgList
), and containing one or more constructors within braces{}
.
The parameters listed after the inductive
keyword represent the index of the inductive type. Since the ultimate part of the index in Saki is always 'Type
, we do not explicitly write it. This mirrors the index definition in CIC, where inductive types can take parameters but always return a value in the universe of types (Type
). For example, in Saki, inductive(A)
corresponds to an index of A -> 'Type
.
General Form of Inductive Types
An inductive type in Saki can be defined in the following general form:
-
IndexParams: Parameters or indices that the inductive type depends on. These can be types or values upon which the type varies.
-
Constructor₁ … Constructorₙ: Constructors that define how values of the inductive type can be constructed. Each constructor has a corresponding type
<ConstructorType>
that may involve the indices and the inductive type itself.
Algebraic Data Type Style vs. Inductive Style Definitions
Inductive types can be defined in different styles. The Algebraic Data Type (ADT) style is more concise and resembles traditional sum types, while the inductive style provides more explicit definitions with indices, providing greater expressiveness and control over the type system.
Consider the Option
type as an example.
ADT Style Definition
In this definition:
Option
is parameterized by a typeA
.None
is a constructor representing the absence of a value.Some
is a constructor that takes a value of typeA
.
Inductive Style Definition
In this definition:
- The constructors explicitly specify their types.
None
has typeOption(A)
, indicated bythis
.Some
is a function fromA
toOption(A)
.
Both definitions are functionally equivalent. The inductive style makes the types of the constructors explicit. This is essential when defining inductive types with indices, as it allows for precise type-level information and reasoning.
Examples of Inductive Types with Indices
Vectors with Length Indices
A classic example of an inductive type with indices is a vector whose length is part of its type. This kind of vector definition ensures that operations on vectors respect their lengths at the type level.
Where:
Vec
is a function that, given a type (A: 'Type
), returns an inductive type with index (n: Int
) representing the length . (i.e. a function taking an integer (n: Int
) and producing an inductive typeVec(n) : 'Type
).- The index is the integer specified in
inductive(Int)
. - Constructors:
Nil
is a constructor of typethis(0)
, representing an empty vector of length 0.Cons
is a constructor that takes an element of typeA
, an integern
, and a vectorVec(A, n)
, and returns a vector of lengthn + 1
, indicated bythis(n + 1)
.
Equality Type
Recall the propositional equality (Leibniz equality) defined as a function in the previous section:
Besides this functional definition, equality can also be defined as an inductive type in Saki:
In this example, \(Eq_A\) and \(refl_x\) are separately defined by the inductive type Eq
and the constructor EqRefl
:
- The
Eq
inductive type has indexA -> A -> 'Type
. - The constructor
EqRefl
provides a proof thatx
is equal to itself (this(x)
), which is equivalent toEq(A, x, x)
.
Practical Example: Verified Merge Sort
/**
* Inductive type definition for natural numbers (ℕ).
* This type is recursively defined using two constructors:
* - `Zero`: The base case representing the natural number zero.
* - `Succ`: The successor constructor, representing a natural number that is one greater than another.
*/
type ℕ = inductive { Zero; Succ(ℕ) }
/**
* Boolean function to compare two natural numbers (ℕ) for less-than-or-equal-to relation (<=).
*
* @param x The first natural number (ℕ).
* @param y The second natural number (ℕ).
* @return true if x is less than or equal to y, false otherwise.
*/
def (<=)(x y: ℕ): Bool = match (x, y) {
case (ℕ::Zero, _) => true
case (ℕ::Succ(x'), ℕ::Succ(y')) => x' <= y'
case (_, _) => false
}
/**
* Inductive type definition for the proof of the less-than-or-equal-to (Leq) relation.
*
* The type provides a constructive proof for the relation `x <= y`:
* - `LeqZero`: A proof that `Zero <= x` for any natural number x.
* - `LeqSucc`: A proof that if `x <= y`, then `Succ(x) <= Succ(y)`.
*/
def Leq: ℕ -> ℕ -> 'Type = inductive(ℕ, ℕ) {
LeqZero : ∀(x: ℕ) -> this(ℕ::Zero, x)
LeqSucc : ∀(x y: ℕ) -> this(ℕ::Succ(x), ℕ::Succ(y))
}
/**
* Inductive type definition for a vector (Vec) of fixed size.
* This type is recursively defined using two constructors:
* - `Nil`: An empty vector of length 0.
* - `Cons`: A non-empty vector with length n + 1, consisting of a head element and a tail vector of size n.
*
* @param n The length of the vector (ℕ).
*/
type Vec(n: Int) = inductive {
Nil // An empty list with length 0
Cons(ℕ, Vec(n)) // A list with length n+1, consisting of a head and a tail
}
/**
* Inductive type definition for the proof that a vector (Vec) is sorted.
*
* The type provides a constructive proof that a vector of length n is sorted:
* - `SortedNil`: A proof that an empty vector is always sorted.
* - `SortedOne`: A proof that a vector with one element is always sorted.
* - `SortedCons`: A proof that a vector with two or more elements is sorted if the head is less than or equal to
* the next element, and the tail is also sorted.
*
* @param n The length of the vector (ℕ).
* @param vec The vector of length n to be checked for sortedness.
* @return A type-level proof that the vector is sorted.
*/
def Sorted(n: Int): Vec(n) -> 'Type = inductive(Vec(n)) {
// An empty list is always sorted
SortedNil: this(Vec(0))
// A list with one element is always sorted
SortedOne: this(Vec(1))
// A list with two or more elements is sorted if the head is less than or equal
// to the next element, and the tail is also sorted
SortedCons: ∀(x y: ℕ) -> ∀(xs: Vec(n)) -> Leq(x, y) -> this(Vec(n)) -> this(Vec(n + 1))
}
/**
* Function to merge two sorted vectors (Vec) and return a merged sorted vector with a proof of sortedness.
*
* @param n The length of the first vector (ℕ).
* @param m The length of the second vector (ℕ).
* @param xs The first sorted vector of length n.
* @param ys The second sorted vector of length m.
* @return A tuple containing the merged vector of length n + m and a proof that the merged vector is sorted.
*/
def merge(n m: Int, xs: Vec(n), ys: Vec(m)): (Vec(n + m), Sorted(n + m)) = match (xs, ys) {
case (Vec(n)::Nil, ys) => (ys, Sorted(m)::SortedNil) // Merging with an empty list is sorted
case (xs, Vec(m)::Nil) => (xs, Sorted(n)::SortedNil) // Merging with an empty list is sorted
case (Vec(n)::Cons(x, xsRest), Vec(m)::Cons(y, ysRest)) => {
if x <= y then {
let (mergedRest, proofRest) = merge(xsRest, ys)
let proof = Sorted(n + m)::SortedCons(x, y, mergedRest, Leq::LeqZero, proofRest)
(Vec(n + m)::Cons(x, mergedRest), proof)
} else {
let (mergedRest, proofRest) = merge(xs, ysRest)
let proof = Sorted(n + m)::SortedCons(y, x, mergedRest, Leq::LeqSucc(x, y), proofRest)
(Vec(n + m)::Cons(y, mergedRest), proof)
}
}
}
/**
* Function to perform merge sort on a vector (Vec) of length n, returning the sorted vector and a proof of sortedness.
*
* @param n The length of the input vector (ℕ).
* @param list The input vector to be sorted.
* @return A tuple containing the sorted vector and a proof that the vector is sorted.
*/
def mergeSort(n: Int, list: Vec(n)): (Vec(n), Sorted(n)) = match list {
// An empty list is trivially sorted
case Vec(0)::Nil => (Vec(0)::Nil, Sorted(0)::SortedNil)
// A single-element list is sorted
case Vec(1)::Cons(x, Vec(0)::Nil) => (list, Sorted(1)::SortedOne)
case _ => {
let (left, right) = split(n, list) // Split the list into two halves
let (sortedLeft, proofLeft) = mergeSort(left)
let (sortedRight, proofRight) = mergeSort(right)
merge(sortedLeft, sortedRight) // Merge the sorted halves with proof
}
}
/**
* Function to split a vector of length n into two sub-vectors.
* The first sub-vector has length n / 2, and the second has length n - n / 2.
*
* @param n The length of the input vector (ℕ).
* @param list The input vector to be split.
* @return A tuple containing the two sub-vectors after the split.
*/
def split(n: Int, list: Vec(n)): (Vec(n / 2), Vec(n - n / 2)) = {
// Start the recursion with initial accumulators and sizes
splitRec(n, n / 2, Vec(n / 2)::Nil, Vec(n - n / 2)::Nil, list)
}
/**
* Recursive helper function to split a vector into two sub-vectors.
* This function takes accumulators and sizes for the left and right sub-vectors, and iterates through the input vector.
*
* @param i The remaining size of the input vector (ℕ).
* @param leftSize The size of the left sub-vector (ℕ).
* @param leftAcc The accumulated left sub-vector (Vec(leftSize)).
* @param rightAcc The accumulated right sub-vector (Vec(n - leftSize)).
* @param rest The remaining part of the vector to be split (Vec(i)).
* @return A tuple containing the two sub-vectors after the split.
*/
def splitRec(
i: Int, leftSize: Int, leftAcc: Vec(leftSize),
rightAcc: Vec(n - leftSize), rest: Vec(i),
): (Vec(n / 2), Vec(n - n / 2)) = match (leftSize, rest) {
// No more elements for the left side, return the remaining as right
case (0, _) => (leftAcc, rest)
// Done splitting, return accumulators
case (_, Vec(i)::Nil) => (leftAcc, rightAcc)
// Add to the left
case (_, Vec(i)::Cons(x, xs)) => {
splitRec(i - 1, leftSize - 1, Vec(leftSize)::Cons(x, leftAcc), rightAcc, xs)
}
}