* [Caml-list] Modular explicits in pre-OCaml 5.5
@ 2026-06-24 4:16 oleg
0 siblings, 0 replies; only message in thread
From: oleg @ 2026-06-24 4:16 UTC (permalink / raw)
To: caml-list
One of the notable features of the just announced OCaml 5.5 are
module-dependent functions, a.k.a. modular explicits. It is a welcome
addition: it lets us express higher-ranked types in the simplest way,
and is particularly good for typed tagless-final code.
It should be mentioned however that modular explicits could be done before,
with little or no hassle. Perhaps there is merit to remind of that old
trick~-- especially because it works also with statically unknown modules
(which are out of scope for OCaml 5.5 modular explicits).
As the running example we re-use the pretty-printing example in the
OCaml 5.5 announcement (hereafter, Ann55).
We start however with a simpler example: pretty-printing a set
generated by the `Set.Make` functor. The example follows the
pattern of the pp_map function from Ann55. It is simpler than
printing a map because it involves no higher-rank types.
let pp_set (type a s)
(module M: Set.S with type elt = a and type t = s)
(pp_elt:Format.formatter->a->unit) (ppf:Format.formatter) (set:s) =
if M.is_empty set then Format.fprintf ppf "ø" else
let pp_sep ppf () = Format.fprintf ppf ",@ " in
Format.fprintf ppf "@[{@ %a@ }@]"
(Format.pp_print_seq ~pp_sep pp_elt) (M.to_seq set)
It is almost literally the pp_map example from Ann55, with set
substituted for map. The main difference is type annotations. This is
not a bug: I insist on writing signatures or explicit type annotations
for all top-level definitions (except, perhaps, the most trivial).
The annotations could be simplified if we introduce
type ('e,'s) set = (module Set.S with type elt = 'e and type t = 's)
type 'a printer = Format.formatter->'a->unit
The example then reads
let pp_set : type e s. (e,s) set -> e printer -> s printer =
fun (module M) pp_elt ppf set -> (* ... as before ... *)
The signature tells at a glance what pp_set is doing (which is one of
the benefits of signatures: it is not just for, and not mainly for,
the compiler.)
We can use pp_set just like pp_map was used in the Ann55 example:
module String_set = Set.Make(String)
let () =
let m = String_set.of_list ["Zero"; "Zero"; "One"; "Un"] in
let pp_str = Format.pp_print_string in
Format.printf "%a@." (pp_set (module String_set) pp_str) m
Our rendition of modular explicits extends beyond statically known
modules like String_set. For example,
(* Abstract set of elements of types 'e. The implementation is abstract *)
type 'e aset = (module Set.S with type elt = 'e)
let f : unit -> int aset = fun () ->
if Random.bool () then
(module Set.Make(Int))
else
(module Set.Make(struct type t = int
let compare x y = - Int.compare x y end))
The function f randomly returns one of two distinct Set
implementations (the Set.t types are not compatible). As an
application, we print a list of integers as a set (automatically
sorting and removing duplicates):
let print_as_set : type a. a aset -> a printer -> a list printer =
fun (module M) pp ppf lst ->
pp_set (module M) pp ppf (M.of_list lst)
let _ = Format.printf "%a@."
(print_as_set (f ()) Format.pp_print_int) [1;2;3;1]
The result is indeed either "{ 1, 2, 3 }" or "{ 3, 2, 1 }", depending
on how die is cast.
Let us now tackle the pp_map example from Ann55. It is challenging because
of the higher-rank type of the map type 'a Map.S.t.
It is tempting to define the module type as
type ('k,'v,'m) map =
(module Map.S with type key = 'k and type 'a t = 'm constraint 'a = 'v)
Alas, it doesn't work:
2 | (module Map.S with type key = 'k and type 'a t = 'm constraint 'a = 'v)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Error: Syntax error: invalid package type: parametrized types are not supported
Package types and their `with' constraints come with many restrictions, which
is deeply unfortunate.
We have to resort to an encoding:
module type maps = sig
include Map.S
type v
type mt
val of_mt : mt -> v t
val to_mt : v t -> mt
end
type ('k,'v,'m) map =
(module maps with type key = 'k and type v = 'v and type mt = 'm)
One should mention that this is a strictly more general type of maps:
it supports specialized implementations for particular combination of
keys and values (e.g., if 'v is bool, we can use Set as the underlying
structure.)
The pretty-printer of maps is the same as in the Ann55, with two additions:
two occurrences of M.of_t.
let pp_map : type k v m. (k,v,m) map -> k printer -> v printer -> m printer =
fun (module M) pp_key pp_v ppf set ->
if M.is_empty (M.of_mt set) then Format.fprintf ppf "ø" else
let pp_sep ppf () = Format.fprintf ppf ",@ " in
let pp_binding ppf (k,v) = Format.fprintf ppf "@[%a@ =@ %a@]" pp_key k pp_v v
in
Format.fprintf ppf "@[{@ %a@ }@]"
(Format.pp_print_seq ~pp_sep pp_binding) (M.to_seq (M.of_mt set))
It applies to statically *unknown* modules however, unlike pp_map in Ann55
type ('k,'v) amap = (module maps with type key = 'k and type v = 'v)
let string_map : type a. unit -> (string,a) amap = fun () ->
(module struct
include Map.Make(String)
type v = a
type mt = v t
let of_mt = Fun.id
let to_mt = Fun.id
end)
let () =
let md = string_map () in
let module M = (val (md : (string,int) amap)) in
let m = M.of_list ["Zero", 0; "One", 1] in
let pp_str = Format.pp_print_int in
Format.printf "%a@."
(pp_map (module M) Format.pp_print_string pp_str) (M.to_mt m)
I should mention that the functions like to_mt come naturally in case
of tagless-final interpreters: these are the observation functions.
For example:
module type lc = sig
type 'a repr
val int : int -> int repr
val lam : ('a repr -> 'b repr) -> ('a -> 'b) repr
val app : ('a -> 'b) repr -> ('a repr -> 'b repr)
type obst
type obs
val observe : obst repr -> obs
end
type ('a,'obs) lc = (module (lc with type obs = 'obs and type obst = 'a))
let ex1 : type obs. (int,obs) lc -> obs = fun (module M) -> let open M in
let t1 = app (lam (fun x -> x)) (int 1)
in observe t1
In conclusion, it would be great if one day the restrictions on
package types were relaxed.
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-06-24 4:19 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-24 4:16 [Caml-list] Modular explicits in pre-OCaml 5.5 oleg
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox