8.6 First-class modules
(Introduced in OCaml 3.12; pattern syntax and package type inferenceintroduced in 4.00; structural comparison of package types introduced in 4.02.;fewer parens required starting from 4.05)
|
Modules are typically thought of as static components. This extensionmakes it possible to pack a module as a first-class value, which canlater be dynamically unpacked into a module.
The expression (modulemodule-expr: package-type) converts themodule (structure or functor) denoted by module expression module-exprto a value of the core language that encapsulates this module. Thetype of this core language value is (modulepackage-type).The package-type annotation can be omitted if it can be inferredfrom the context.
Conversely, the module expression (valexpr: package-type)evaluates the core language expression expr to a value, which musthave type modulepackage-type, and extracts the module that wasencapsulated in this value. Again package-type can be omitted if thetype of expr is known.If the module expression is already parenthesized, like the argumentsof functors are, no additional parens are needed: Map.Make(val key).
The pattern (modulemodule-name: package-type) matches apackage with type package-type and binds it to module-name.It is not allowed in toplevel let bindings.Again package-type can be omitted if it can be inferred from theenclosing pattern.
The package-type syntactic class appearing in the (modulepackage-type) type expression and in the annotated forms represents asubset of module types.This subset consists of named module types with optional constraintsof a limited form: only non-parametrized types can be specified.
For type-checking purposes (and starting from OCaml 4.02), package typesare compared using the structural comparison of module types.
In general, the module expression (valexpr: package-type) cannot be used in the body of a functor, because this could causeunsoundness 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 thisexpression can be used anywhere, and even if it contains such typesit can be used inside the body of a generativefunctor, described in section 8.16.It can also be used anywhere in the context of a local module bindingletmodulemodule-name=(val expr1: package-type)in expr2.
Basic example
A typical use of first-class modules is toselect at run-time among several implementations of a signature.Each implementation is a structure that we can encapsulate as afirst-class module, then store in a data structure such as a hashtable:
type picture = … module type DEVICE = sig val draw : picture -> unit … end let devices : (string, (module DEVICE)) Hashtbl.t = Hashtbl.create 17 module SVG = struct … end let _ = Hashtbl.add devices "SVG" (module SVG : DEVICE) module PDF = struct … end let _ = Hashtbl.add devices "PDF" (module PDF: DEVICE)
We can then select one implementation based on command-linearguments, for instance:
let parse_cmdline () = … module Device = (val (let device_name = parse_cmdline () in try Hashtbl.find devices device_name with Not_found -> Printf.eprintf "Unknown device %s\n" device_name; exit 2) : DEVICE)
Alternatively, the selection can be performed within a function:
let draw_using_device device_name picture = let module Device = (val (Hashtbl.find devices device_name) : DEVICE) in Device.draw picture
Advanced examples
With first-class modules, it is possible to parametrize some code over theimplementation of a module without using a functor.
let sort (type s) (module Set : Set.S with type elt = s) l = Set.elements (List.fold_right Set.add l Set.empty) val sort : (module Set.S with type elt = 'a) -> 'a list -> 'a list = <fun>
To use this function, one can wrap the Set.Make functor:
let make_set (type s) cmp = let module S = Set.Make(struct type t = s let compare = cmp end) in (module S : Set.S with type elt = s) val make_set : ('a -> 'a -> int) -> (module Set.S with type elt = 'a) = <fun>