As defined in chapter 7 of the book, a modality is an operation on types that behaves somewhat like the n-truncation. Specifically, it consists of a collection of types, called the modal ones, together with a way to turn any type into a modal one , with a universal property making the modal types a reflective subcategory (or more precisely, sub-(∞,1)-category) of the category of all types. Moreover, the modal types are assumed to be closed under Σs (closure under some other type formers like Π is automatic).
We called them “modalities” because under propositions-as-(some)-types, they look like the classical notion of a modal operator in logic: a unary operation on propositions. Since these are “monadic” modalities — in particular, we have rather than the other way around — they are most closely analogous to the “possibility” modality of classical modal logic. But since they act on all types, not just mere-propositions, for emphasis we might call them higher modalities.
The example of n-truncation shows that there are interesting new modalities in a homotopy world; some other examples are mentioned in the exercises of chapter 7. Moreover, most of the basic theory of n-truncation — essentially, any aspect of it that involves only one value of n — is actually true for any modality.
Over the last year, I’ve implemented this basic theory of modalities in the HoTT Coq library. In the process, I found one minor error in the book, and learned a lot about modalities and about Coq. For instance, my post about universal properties grew out of looking for the best way to define modalities.
In this post, I want to talk about something else I learned about while formalizing modalities: Coq’s modules, and in particular their universe-polymorphic nature. (This post is somewhat overdue in that everything I’ll be talking about has been implemented for several months; I just haven’t had time to blog about it until now.)
It took me a long time to get any idea of what Coq’s modules are and what they are for. I didn’t find the documentation in the Coq reference manual to be very helpful: it seems to be written for a reader who already knows what modules are and only wants to know the appropriate syntax to use them in Coq. The general idea of modules seems to be inherited from ML; but in ML modules seem mainly to be used for things that in Coq I would be inclined to use dependent records or typeclasses for, like defining “a group” to be a type together with appropriate operations and axioms. So why, I wondered, does Coq have modules at all?
One reason is for namespacing. Unlike a record, a module can be “imported”, so that all of its fields can be accessed without having to mention the module’s name all the time. In fact, every file in Coq is implicitly its own module, and when you say Require Import Filename. you are actually importing a module. Similarly, modules are used for access control in Dan Licata’s private-inductive-types hack that we use to define HITs that compute on point constructors.
I don’t know whether there are reasons other than this that modules were introduced into Coq, or other purposes that other people use them for. However, over the past year I’ve discovered two other uses for them, one of which I’ll discuss in this post and another of which I’ll postpone until a later one. But first, let me tell you something about what modules are.
Coq has not just modules, but “module types”. To first approximation, it is appropriate to think of a Module Type as analogous to a Record type, and a Module having that module type (called an “implementation” of it) as analogous to an element of that record type. For instance, instead of
Record foo := { bar : Type ; baz : bar -> Type }.
we could write
Module Type foo. Parameter bar : Type. Parameter baz : bar -> Type End foo.
and then instead of
Definition qux : foo := Build_foo Bool (fun b => if b then Unit else Empty).
we could write
Module qux <: foo. Definition bar : Type := Bool. Definition baz : bar -> Type := fun b => if b then Unit else Empty. End qux.
Given these definitions, where we refer to bar qux and baz qux in the record case, in the module case we would write qux.bar and qux.baz. However, there are a few essential differences (apart from these syntactic ones).
Firstly, while elements of records are (like everything else in Coq’s type theory) strongly typed, modules are duck-typed. (Edit: Technically it may be structural typing instead; see the comments below.) In other words, qux is a module of type foo simply by virtue of containing fields named bar and baz that have the same types as those declared for the parameters of foo; the type declaration “<: foo” only serves to document and enforce this fact.
Secondly, modules do not have to be declared to have any type, or they can have more than one type. A module is free to contain as many definitions (and other things such as notations, coercions, instances, etc.) as you like, and to “implement” as many module types as you like. In particular, qux could contain additional definitions and it would still be of type foo.
Thirdly, and more importantly, modules are second-class: you cannot pass them around as arguments to functions. Nor can you construct them “on the fly”; they can only be defined at top level. (You can define them inside other modules, but not inside sections.) However, you can pass a module as an argument to another module. For instance, here is a module which takes a module of type foo as an argument.
Module htns (f : foo). Definition qjkx : Type := { x : f.bar & f.baz x }. End htns.
(Confusingly, modules that take other modules as arguments are traditionally called “functors”, but there is no requirement that they actually be functorial in the category-theorist’s sense, nor indeed do modules of a general given module-type even form a category in any nontrivial way.) Now if we have a foo, such as qux, we can pass it as an argument to htns and get a new module (again, only at top level):
Module gcrl := htns qux.
After this, we can refer to gcrl.qjkx and get { x : qux.bar & qux.baz x }. Together with the fact that modules don’t need to have a type, this sort of gives us a way to pass a module as an argument to a collection of functions; we can define a module like htns which takes a foo as an argument and in which we define many functions depending on this foo. Then whenever we want to apply these functions to a particular foo (such as qux) we do the application at top-level, as above with gcrl.
Coq does not allow modules to take elements of ordinary types as arguments. If you want to pass a nat, say, as an argument to a module, you have to first wrap the nat in another module, e.g. with
Module natM. Parameter m : nat. End natM. Module timesM (mynat : natM). Definition f : nat -> nat := fun n => n * mynat.m. End timesM.
You can think of types and module-types as “parallel universes” of types; never the twain shall meet.
These limitations seem quite annoying at first. So apart from namespacing (which doesn’t generally need module types at all), why would we use modules instead of records? For modalities, the main reason is that (at least in Coq 8.5, with universe polymorphism as implemented by Mathieu Sozeau) the fields of a module are individually universe polymorphic. In other words, in order to define a module of type foo, as above, you need to give a polymorphic definition of bar and a polymorphic definition of baz, and the resulting module remembers the polymorphism of each of those fields. By contrast, a definition of an element of a record type may be itself polymorphic, but an individual instance of that definition will pertain only to a fixed collection of universes.
Note that the possibility of individually polymorphic fields practically mandates that modules must be second-class. For a polymorphic field involves an implicit quantification over all universes; hence if the record itself were a first-class object, what universe would it live in? I like to think of modules as analogous to the proper classes in NBG set theory: they can be “large” without impacting the consistency strength, because we are limited in what we can do with them. (However, this is at present only an analogy; I am not aware of any precise theoretical study of modules.)
In the case in point, if a modality were a record, then “a modality” would be a modality acting on types in only one universe. A polymorphic definition of a particular modality would result in defining related modalities on every universe, but the relation between these modalities would not be specified. In particular, if we have types X : Type@{i} and Y : Type@{j} in different universes and a map f : X -> Y, where Y is modal in Type@{j}, we could not apply the universal property to extend f to a map O X -> Y, since the universal property asserted for O@{i} X would only refer to maps with target also in Type@{i}.
This is at best annoying, and at worst unworkable. I tried for a while to make it work with various hacks, such as parametrizing a modality by two universes rather than one, but eventually I gave up. (The place where it became unworkable for me was in proving that for a left exact accessible modality, the universe of modal types is modal, which irreducibly involves applying the same modality at two different universe levels.)
Instead, we can make a modality a module. Specifically, we make Modality a module type, with each modality an instantiation of it. This means that in order to define “a modality”, you have to give a polymorphic definition of the reflector, the universal property, etc. In particular, the universal property must be polymorphic enough to allow the situation with X : Type@{i} and Y : Type@{j} considered above, and thus resolves all the problems I was having.
There are admittedly some issues involving this choice. One is the fact, mentioned above, that a module cannot be parametrized over an ordinary type. However, we do sometimes want to define a family of modalities, e.g. the n-truncation modalities for all n : trunc_index. A solution is for our basic Module Type to represent not a single modality, but an entire family of them, parametrized by some type. Thus it is actually called Modalities and contains a field Modality : Type to do the parametrization. Then we define all the n-truncation modalities at once by instantiating this module type with Modality := trunc_index. I think this can be regarded as analogous to how when doing mathematics relative to a base topos, the correct notion of “large category” is an indexed category (a.k.a. fibration), which comes with a basic notion of “I-indexed family of objects” for all I in the base topos.
Another issue is that a polymorphic field of a module must be fully polymorphic. For instance, in an instantiation of foo as above, the definition of bar must be a type that lives in every universe. In particular, therefore, one cannot define bar := Type. It would be ideal if a module type could “declare a universe parameter” that different instantiations could implement differently. This might be possible in the future, but for now I hacked around it using definitions such as Type2, which is defined by the HoTT Coq library to be a (polymorphic) universe that has at least one universe strictly below it.
A third, even more problematic, issue is that when implementing a polymorphic module type, Coq is very strict about matching up the polymorphism. Specifically, each Definition in the implementing module must have exactly the same number of universe parameters as the corresponding Parameter in the module type, and all the constraints in the former must be implied by those in the latter. This ensures that the implementation is “at least as polymorphic” as the specification, and when you think about it enough it’s hard to see how things could be much otherwise.
Normally, however, a universe-polymorphic definition in Coq ends up with many more universes than it needs, and we have little control over how many those are. Moreover, the number of universe parameters is currently rather fragile with respect to small changes. (In fact, it can sometimes happen that the interactive coqtop and the compiler coqc give the same definition a different number of universes! Although this is regarded as a bug.) Therefore, in order to have a chance of ensuring that our implementations of module types match up in polymorphism, we almost always need to add explicit universe annotations to control how many universe parameters they end up with. This is annoying and tedious, requiring much manual tracing through of proofs with copious use of Set Printing Universes and Show Universes. Moreover, sometimes a definition ends up with extra “bigger” universe parameters that can’t be eliminated, such as to serve as a “maximum” of two or more “real” universe parameters, or to serve as the target for a recursively defined type family; this sometimes forces us to hackishly introduce spurious universe parameters in our module types to match our desired instantiations.
However, universe polymorphism is a very new feature in Coq, so there’s hope that things will get better over time. It’s tempting to think the problems would be reduced with a more explicit sort of universe polymorphism such as that available in Agda (or, I believe, Lean) — but unfortunately, neither Agda nor Lean has modules that can be used in this way. Agda’s modules are more like Coq’s Sections with some additions like namespace control, simultaneous application, and binding records; in particular, there are no module types. And Lean has decided against including anything like Coq’s modules. So even though we Coq users seem to be in the minority for HoTT formalization these days, it seems I’ll be unlikely to switch my allegiance in the near future. (-:
Update: It’s been pointed out on the mailing list that while Agda’s modules can’t be used like Coq’s, Agda actually has another feature that might be usable for this purpose. Namely, in Agda there is a “” in which all the universes live (where is the variable that universe polymorphism quantifies over), and one can actually apply s to types in freely. Thus, universe-polymorphic functions, such as those that constitute a modality, can be given as arguments to a function.
It would be a little awkward because we can’t apply type constructors other than to such “large objects”, so in particular we couldn’t “package up” all the parts of a modality into a single record: any function defined for all modalities would have to take five or six arguments that make up the modality. But Agda’s modules could probably be used to reduce that annoyance somewhat.
I have not actually tried this yet, and probably won’t have time to in the near future, but if anyone else wants to I think it would be a great project.