Alright kittens, I've been meaning to write this up for a while. Let's do a proper deep dive into sized types in Agda โ specifically how they help us control coinductive productivity in a way that's actually compositional and modular, unlike Agda's older syntactic guardedness checker.
The core motivation: coinductive types in Agda denote greatest fixpoints. If you want to define infinite structures (streams, potentially infinite trees, etc.), you need corecursion. But Agda is a total language โ every program must terminate or be productive. The guardedness checker is the traditional answer, but it has sharp edges.
Let's start with the classic example. A stream without sized types:
record Stream (A : Set) : Set where coinductive field head : A tail : Stream A -- This works fine repeat : โ {A} โ A โ Stream A head (repeat x) = x tail (repeat x) = repeat x -- This might be REJECTED by the guardedness checker! zipWith : โ {A B C} โ (A โ B โ C) โ Stream A โ Stream B โ Stream C head (zipWith f xs ys) = f (head xs) (head ys) tail (zipWith f xs ys) = zipWith f (tail xs) (tail ys)
The guardedness checker works syntactically โ it needs corecursive calls to be directly under a constructor. Once you pass a corecursive result through a helper function, the checker may lose track of the guard. This is where sized types come in.
Sized types track the "depth" of a coinductive structure via an ordinal-like index. The key primitives in Agda are:
-- Enable with: {-# OPTIONS --sized-types #-} open import Size -- Size : Set (the type of sizes) -- โ : Size (the "infinite" size / greatest element) -- โ_ : Size โ Size (successor of a size) -- Size< i : Set (sizes strictly less than i) -- A sized stream: record Stream (A : Set) (i : Size) : Set where coinductive field head : A tail : โ {j : Size< i} โ Stream A j
Now Stream A i is a stream defined to depth at least i. A value of type Stream A โ is a fully productive infinite stream. The sized version of zipWith typechecks beautifully:
zipWith : โ {A B C} {i : Size} โ (A โ B โ C) โ Stream A i โ Stream B i โ Stream C i head (zipWith f xs ys) = f (head xs) (head ys) tail (zipWith f xs ys) {j = j} = zipWith f (tail xs) (tail ys) -- tail xs : Stream A j, with j : Size< i โ structurally decreasing!
The size index acts as a certificate: tail xs has type Stream A j for j < i, so we're definitely making progress. The type system handles what the syntactic checker couldn't.
I'll follow up with the Abel-Pientka paper discussion. Who's read it?