Default argument to functors?

I’m trying to create a functor with a sort of polymorphic default
argument, but having no luck.

Here’s the failed idea:

First, a module with a pretty printer for a container element:

module type PP =
sig
   type t
   val pp : t Fmt.t
end

And then the most general functor for an ordered container:

module Make2 (Pp : PP) (Ord : Set.OrderedType with type t = Pp.t) =
struct
   type elt = Pp.t
   type t =
   | Empty
   | Tree of elt * int * t * t
   (* ... *)
end

Now the polymorphic “default” argument for Pp in Make2:

module Default_pp =
struct
   type 'a t  (* XXX *)
   let pp ppf _ = Fmt.string ppf "<element>"
end

And the simpler functor providing convenience over configurability:

module Make = Make2(Default_pp)

… used like:

module IntOrd = struct type t = int let compare = Int.compare end
module IntM = Make(IntOrd)

module FloatOrd = struct type t = float let compare = Float.compare end
module FloatM = Make(FloatOrd)

But the Make2 application has the error:

Modules do not match:
sig type 'a t = 'a Default_pp.t val pp : Format.formatter -> 'a -> unit end
is not included in PP
Type declarations do not match:
type 'a t = 'a Default_pp.t
is not included in
type t

I realize the extra 'a parameter is the problem, but I don’t know how
to specify the correct polymorphic type. I’m assuming I need a
polymorphic type here for it to serve as a “default”, but maybe
there’s an alternative using type constraints in the signature of Make
or Make2.

How could this be done, either along the above lines, or in some other
completely different way?

I hope someone can help. Thanks,

In your definition the element type is derived from the printer type. This cannot work with a default printer for any type. It is simpler to reverse the dependency, then remove the type definition from the Pp argument which allows you to define a Default_printer module with type sig val pp: 'a Fmt.t:

module Make2 (Ord : Set.OrderedType)(Pp: PP with type t := Ord.t) = struct
  type t = Ord.t
  ...
end
module Default_printer = struct
  let pp ppf _ = Fmt.string ppf "<element>"
end
module MakeD(Ord:Set.OrderedType) = Make2(Ord)(Default_printer)

Thank you! It’s obvious now that you’ve pointed it out (isn’t it always :grinning_face:).

Maybe it’ll be useful to somebody to get feedback on what tripped up a beginner like myself. I actually did start with the dependency reversed as you’ve done, though I used a simple type constraint (=) rather than your destructive substitution.

What I had the most trouble with was getting the application of Make2 right. In retrospect, what you give should have been obvious, in a direct parallel with function application (let makeD ord = fun pp -> make2 ord pp), but I had a few “inter-tangled” troubles.

First, I was unsure about the syntax for working with functors with multiple arguments. I didn’t find clarity in the introduction section in the Ocaml Language Manual or in Real World Ocaml. Second, I wasn’t sure how to properly specify the needed type constraint; I knew I had to add the constraint in the signature of Make 2, but when that didn’t seem to work, I then tried to add an additional constraint at the point of application of Make2–variations of something like Make2(Default_printer with type t = Ord.t). Finally, I was stumped by some perplexing compiler error messages at the application of Make2, including a particularly mysterious error about a signature mismatch between an empty signature (... sig end) and a parameter of Make2–where did the empty signature come from, I thought, when I had clearly provided a non-empty module in the application of Make2.

In summary, I think it would have helped me to have seen an example of using a functor with multiple arguments and some accompanying text describing the parallels between functors and functions, such as

  • module M A B is module A : functor A -> functor B -> ... (right?) like let f a b is let f = fun a -> fun b -> ... which seems to suggest you can curry functors (right?)
  • M(A)(B) is like f a b
  • module M A (B with type t = A.t) is sort of like let f (type c) (a : c t) (b : c t)
  • module M B = M2(A)(B) is like let f b = fun a -> g a b

I don’t think it would be fair to bash compiler error messages, given how complexly the various pieces of the module language can interact, but having the foundations above would allow a beginner like myself to better grasp what the error message is trying to tell me and to better guide me in finding the one or two small pieces that aren’t fitting exactly right.