(First-class modules were introduced in OCaml 3.12; pattern syntax and
package type inference in 4.00; structural comparison of package types
in 4.02; fewer parentheses required starting from
4.05.
Module-dependent functions were introduced in 5.5.)
Module-dependent functions and first-class modules make it possible to manipulate modules as values, in particular to accept modules as function parameters.
One can write a function sort0 that sorts elements by adding them into a sorted set, and then reads them back in order:
Using module-dependent functions, it is also possible to express sort as a function parameterized over an ordered-set module. This lets callers choose their ordered-set implementation, which may be specialized for their type; for example sets of integers can be represented with bitsets, or with compact representations of dense intervals.
The declaration of sort uses the pattern syntax (module M : S), which allows function parameters to be modules instead of ordinary values. We call sort a module-dependent function because in its type, (module MSet : Set.S) -> MSet.elt list -> MSet.elt list, the module parameter appears in the right-hand-side (return type) of the arrow (module MSet : Set.S) -> ..., via occurrences of the type MSet.elt.
To apply the sort function, we use the construction (module M) to provide the argument, or (module M : S) to be explicit about the intended signature.
This provides a lightweight alternative to functors. Indeed, the sort function could also be expressed using a Sort functor:
Module-dependent functions offer a nice way to implement code that is parametric over families of type structures, for example over any monad.
In the function application sort (module ISetIncr) from our example above, we say that the module argument is static: it is known at compile-time.
First-class modules let you build dynamic values that represent modules, where the choice of module depends on runtime computations. If M is a module of signature S, then (module M : S) is an expression of type (module S), we say that it “packs” the module M as a value.
A typical use of first-class modules is to select at run-time among several implementations of a signature. Each implementation is a structure that we can pack into a first-class module, then store in a data structure such as a hash table:
The module expression (val e : S) can then unpack a packed-module value (e : (module S)) back into a module. In our example, one implementation can be selected based on command-line arguments:
Alternatively, the selection can be performed within a function:
There is a restriction on module-dependent functions fun (module A : S) -> ...: if the type of the body of the function (the ...) depends on the module argument A, then this function is only allowed to take static module arguments of the form (module M : S), rather than arbitrary expressions of type (module S). There is no such restriction if the type of the body does not mention A, or if the type-checker can remove the occurrences of A by inlining type equalities (for example when A.t equals int).
Our sort function of Paragraph 12.5.1 is such a module-dependent function: its type is (module MSet : Set.S) -> MSet.elt list -> MSet.elt list, where the module MSet is used on the right of the arrow (module MSet : Set.S) -> .... The following example is rejected:
In some cases, including this example, it is possible to rewrite the code to use a static module argument again:
Notice that MSet.elt cannot occur in the type of okay, as the module MSet is only defined locally. In this example the return type of sort does mention MSet.elt, but this type is equal to int so it can be simplified away.
Finally, it is also possible to remove the dependency from the definition of sort, by replacing the argument signature Set.S with the signature Set.S with type elt = a, which uses a locally abstract type (type a) (See 12.4). This was the only approach available before module-dependent functions were introduced in the language, but it is less flexible as it requires changing the function definition and not just the call-site.
Note that removing dependencies using (type a) like this can only be done for non-parameterized types like elt, not with parameterized types like 'a t; there is no corresponding (type 'a t) abstraction. It would not be possible for the mapM example above, which has a dependency on a module containing a parameterized type 'a M.t, and thus cannot be passed dynamically-selected module arguments.
The following example fails, where sort is our module-dependent function from before:
The problem comes from subtle details in the type-checking of n-ary function applications. This is checked as Fun.id to which three parameters are passed, and only the type of Fun.id (not its arguments) is used to resolve the module application, so no module-dependent function type is known at that point.
The example can be fixed by first applying Fun.id sort, and then passing the module argument in a separate application. Then the module-dependent type of Fun.id sort will be used to check the module application. This can be done by simply adding extra parentheses:
(Explicitly annotating the type of ‘Fun.id‘ with a module-dependent function type would also work.)
Before OCaml 5.5, module-dependent functions were not supported. One could use first-class modules, but functions taking a first-class module had to be non-dependent, so this (type a) workaround was often used.
|
The expression ( module module-expr : package-type ) converts the module (structure or functor) denoted by module expression module-expr to a value of the core language that packs this module. The type of this core language value is ( module package-type ). The package-type annotation can be omitted if it can be inferred from the context.
Conversely, the module expression ( val expr : package-type ) evaluates the core language expression expr to a value, which must have type module package-type, and extracts the module that was encapsulated in this value. Again package-type can be omitted if the type of expr is known. If the module expression is already parenthesized, like the arguments of functors are, no additional parens are needed: Map.Make(val key).
The pattern ( module module-name : package-type ) matches a package with type package-type and binds it to module-name. It is not allowed in toplevel let bindings. A function whose parameter has this exact pattern can be module-dependent. Again package-type can be omitted if it can be inferred from the enclosing pattern.
The package-type syntactic class appearing in the ( module package-type ) type expression, in ( module module-name : package-type ) -> typexpr type expression and in the annotated forms represents a subset of module types. This subset consists of named module types with optional constraints of a limited form: only non-parameterized types can be specified.
For type-checking purposes (and starting from OCaml 4.02), package types are compared using the structural comparison of module types.
When typechecking we also consider ( module module-name : package-type ) -> typexpr and ( module package-type ) -> typexpr to be identical when module-name can be erased from typexpr.
In general, the module expression ( val expr : package-type ) cannot be used in the body of a functor, because this could cause unsoundness in conjunction with applicative functors. Since OCaml 4.02, this is relaxed in two ways: if package-type does not contain nominal type declarations (i.e. types that are created with a proper identity), then this expression can be used anywhere, and even if it contains such types it can be used inside the body of a generative functor, described in section 12.15. It can also be used anywhere in the context of a local module binding let module module-name = ( val expr1 : package-type ) in expr2.