Skip to content

Indexed data and total functions

This tutorial builds a length-indexed vector and uses its index to rule out invalid states. The statistical reason to care is familiar: shape, missingness, and support assumptions often begin as comments or runtime checks. An indexed family can make one of those assumptions part of the computation's type.

We will build the length-indexed summary (LIS): a function whose input type records the vector length and whose recursive branches preserve that length witness.

1. Declare the index

Nat is a closed index sort. Z represents zero and S(n) its successor.

index Nat = Z | S(Nat)

family Vec[A : Type](n : Nat) : Type
    constructor Nil : Vec[A](Z)
    constructor Cons[m : Nat] : A * Vec[A](m) -> Vec[A](S(m))

define length[A : Type, n : Nat](xs : Vec[A](n)) : Int !{} =
    case xs motive (m : Nat) => Int
        Nil =>
            return 0
        Cons[k](head, tail) =>
            let rest <- length[A, k](tail)
            return 1 + rest

The family has one uniform parameter A and one refinable index n. Nil constructs only Vec[A](Z). Cons[m] accepts a tail of length m and constructs a vector of length S(m).

The case motive says that every branch returns Int, regardless of the refined length. In the Cons branch, tail has type Vec[A](k), so the recursive call must be specialized at k.

2. Fold without a shape check

Add a real-valued fold:

index Nat = Z | S(Nat)

family Vec[A : Type](n : Nat) : Type
    constructor Nil : Vec[A](Z)
    constructor Cons[m : Nat] : A * Vec[A](m) -> Vec[A](S(m))

define sum[n : Nat](xs : Vec[Real](n), acc : Real) : Real !{} =
    case xs motive (m : Nat) => Real
        Nil =>
            return acc
        Cons[k](head, tail) =>
            let rest <- sum[k](tail, acc + head)
            return rest

No branch asks whether the tail is empty, and no runtime integer is decremented. The constructor proof selects the valid recursive argument.

3. Build a closed vector

Constructor values state their family application explicitly:

index Nat = Z | S(Nat)

family Vec[A : Type](n : Nat) : Type
    constructor Nil : Vec[A](Z)
    constructor Cons[m : Nat] : A * Vec[A](m) -> Vec[A](S(m))

define triple(a : Real, b : Real, c : Real) : Vec[Real](S(S(S(Z)))) !{} =
    let last = construct Cons[Real, Z](c, construct Nil[Real]() as Vec[Real](Z)) as Vec[Real](S(Z))
    let pair = construct Cons[Real, S(Z)](b, last) as Vec[Real](S(S(Z)))
    return construct Cons[Real, S(S(Z))](a, pair) as Vec[Real](S(S(S(Z))))

The static arguments to Cons are the uniform element type followed by its local predecessor index. The as annotation closes the resulting family application.

4. Run the summary

The complete source is indexed-vectors.qvr. Check it, list its entries, and invoke the pure summarize computation:

qvr check docs/tutorials/qvr/source/indexed-vectors.qvr
qvr run docs/tutorials/qvr/source/indexed-vectors.qvr --list
qvr run docs/tutorials/qvr/source/indexed-vectors.qvr summarize 1.5 2.0 3.5 --json

The same entry boundary is available in Python:

from quivers.dsl import load

indexed = load("docs/tutorials/qvr/source/indexed-vectors.qvr")
run = indexed.run("summarize", 1.5, 2.0, 3.5)
assert run.value == (7.0, 3)
print(run.value)

run.entry.kind is "computation", the runtime is core, and the trace contains the recursive calls as checked evaluator events.

5. Transfer the pattern to statistical data

Length is only one useful index. Missingness can be made explicit with Measurement(Observed) and Measurement(Missing); censoring can distinguish Exact, Left, Right, and Interval; a covariance factor can distinguish Raw, Cholesky, and PositiveDefinite states. The design test is whether a constructor establishes an invariant that later case analysis can use.

A potential worry is that the index merely moves validation into more syntax. For data arriving from an untyped host, a boundary check is still necessary. But after that check constructs the indexed value, every downstream computation receives the refined type, so the invariant is checked once rather than rediscovered at each use.

Next, Effects and handlers adds an explicit interpretation to an operation while keeping the calling computation abstract. The type reference gives the complete family and case rules.