2 D. Gratzer et al.
But unfolding definitions is not merely a matter of convenience: to prove a
property of a function, we must unfold it. For example, if we make the definition
of the
(+)
function opaque, then
(+)
is indistinguishable from a variable of type
N→N→Nand so cannot be shown to be commutative, satisfy 1 + 1 = 2,etc.
In practice, proof assistants resolve this contradiction by adopting an inter-
mediate stance: definitions are transparent (unfolded during type checking) by
default, but users are given some control over their unfolding. Coq provides
conversion tactics (
cbv
,
simpl
,etc.) for applying definitional equalities, each
of which can be annotated with a list of definitions to unfold; its
Opaque
and
Transparent
commands toggle the default unfolding behavior of a transparent
definition; and the SSReflect tactic language natively supports a “
lock
ing”
idiom for controlling when definitions unfold [
17
]. Agda allows users to group
multiple definitions into a single
abstract
block, inside of which the definitions
are transparent and outside of which they are opaque; this allows users to define
a function, prove all lemmas that depend on how the function was defined, and
then irreversibly make the function and lemmas opaque.
These mechanisms for controlling unfolding are more subtle than they may at
first appear. In Agda, definitions within
abstract
blocks are transparent to other
definitions in the same block, but opaque to the types of those definitions; without
such a stipulation, those types may cease to be well-formed when the earlier
definition is made opaque. Furthermore,
abstract
blocks are anti-modular,
requiring users to anticipate all future lemmas about definitions in a block—
indeed, the Agda standard library [
37
] uses
abstract
exactly once at the time of
writing. Coq’s conversion tactics are more flexible than Agda’s
abstract
blocks,
but being tactics, their behavior can be harder to predict. The
lock
idiom in
SSReflect is more predictable because it creates opaque definitions, but comes
in four different variations to simplify its use in practice.
Contributions We propose a novel mechanism for fine-grained control over the
unfolding of definitions in dependent type theory. We introduce language-level
primitives for controlled unfolding that are elaborated into a core calculus with
extension types [
30
], a connective first introduced in the context of homotopy
type theory. We justify our elaboration algorithm by establishing a normalization
theorem (and hence the decidability of type checking and injectivity of type
constructors) for our core calculus, and we have implemented our system for
controlled unfolding in the experimental cooltt proof assistant [29].
Definitions in our framework are opaque by default, but can be selectively
and locally unfolded as if they were transparent. Our system is finer-grained and
more modular than Agda’s
abstract
blocks: we need not collect all lemmas that
unfold a given definition into a single block, making our mechanism better suited
to libraries. Our primitives have more predictable meaning and performance
than Coq’s unfolding tactics because they are implemented by straightforward
elaboration into the core calculus (via new types and declaration forms); we
anticipate that this type-theoretic account of controlled unfolding will also provide
a clear path toward integrating our ideas with future language features.