Literally every time I try to pass a module as a parameter, I get errors about constructors escaping the scope. What does it mean and how to avoid?

I recently wrote something comparing module functors and to Haskell’s type classes, and I used a sum function as an example of how one might make a polymorphic function in OCaml.

module type Foldable = sig 
  type 'a t 
  val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b t -> 'a 
end 
 
module type Addable = sig 
  type t 
  val add : t -> t -> t 
  val zero : t 
end 
 
module Summing (F : Foldable) (A : Addable) = struct 
  let sum nums = F.fold_left A.add A.zero nums 
end 

All well and good. Later, as an exercise for myself, I tried to refactor this using modules as a function argument.

let sum (module F : Foldable) (module A : Addable) (nums : A.t F.t) : A.t =
  F.fold_left A.add A.zero nums

And of course I got this error regarding nums

Error: This pattern matches values of type A.t F.t
       but a pattern was expected which matches values of type 'a
       The type constructor F.t would escape its scope

What does it mean? Is the function not the scope of the constructor? How does one even use first-class modules if this is not possible? What are they for? Is there a way to get this code to work?

I’m very confused by all of this because I have literally never succeeded in implementing anything with a first-class module.

I advise reading the current work on modular explicits by Samuel Vivien (this PR has a good introduction explaining the issues). Most of the complexity about that resolves around the issue you’re encountering: a qualified type (e.g. F.t) is only valid in scopes where F is defined, and in vanilla OCaml the type t1 -> t2 must have both t1 and t2 valid in the outer scope, so when t1 is a first class module t2 cannot contain any mention of it.

The usual workaround for the simple situations is to use type variables:

let sum (module A : Addable with type t = 'a) (x : 'a) (y : 'a) : 'a =
  A.add x y

But that doesn’t work with parametric types like Foldable.t, for which you’re stuck with functors until modular explicits land.

3 Likes

With first-class modules, you usually need to use locally abstract types. Example:

let add (type a) (module A : Addable with type t = a) (a : A.t) (b: A.t) : A.t =
  A.add a b

However, as mentioned above, parametric types are not possible with first-class modules.

2 Likes

Thanks for the link! I will read over it. I personally don’t have any problem with functors, so I don’t feel “stuck” with them, so to speak.

I guess part of what I don’t understand is why we have this feature when it’s similar to functors but less powerful. Is there any case when a first-class module is a better option than a functor?

A first-class module is really more analogous to a record containing functions, so I think a better question is: are there any cases where a first-class module is better than a record?

The key difference between those is that a module can contain types, whereas record fields can only contain values. If you want type information passed around in a first-class way, then a module is what you need. There are also some cases where a module may be naturally more ergonomic, since you may already have a module defined anyway so there’s no point in creating a new record.

The main advantages of functors is that they can create new types and work with parametric types.

2 Likes

First-class modules are ordinary values, that can depend on other values.

In particular, the types defined inside a first-class module cannot be known outside
of the packed module in general. It is better to think of them as internal types of the first-class
module value that cannot be leaked to the outside world (outside of the limited with type t = 'a constraint that allow to link a internal type with a normal type).

For instance, one may define

let random () : (module Addable) =
  if Random.bool () then 
    (module struct type t = float let zero = 0. let add = (+.) end)
  else
    (module struct type t = string let zero = "" let add = (^) end)

which illustrates that the function sum cannot be well-typed, otherwise the type of

let s = sum(module List)(random ())

would depend on the random value generated at runtime.

5 Likes

I guess this makes sense. I saw some examples with a list of modules, and this could conceivably be useful.

I guess it’s kind of like GADTs with existential parameters—a sort of way to do runtime polymorphism without using objects—although using objects is not the end of the world either.