Combining independent extension functors

I’m trying to make independent functors that take a module as argument and return a similar module with a new value. I’d like to be able to chain such functors in a way that keeps the contribution of each one without having to define signatures for each possible combination.

I guess that could also be seen as a kind of polymorphism for module signatures. Anyway, here’s an example to clarify what I mean:

module type X = sig
  val x : int
end

module Extend_with_y(M : X) = struct
  include M

  let y = 2
end

module Extend_with_z(M : X) = struct
  include M

  let z = 3
end

module A = struct
  let x = 1
end
module B = Extend_with_y(A)
module C = Extend_with_z(B)

let () =
  Printf.printf "%d, %d, %d\n" C.x C.y C.z

Currently, this results in:

File "ocaml_sig_param.ml", line 25, characters 35-38:
25 |   Printf.printf "%d, %d, %d\n" C.x C.y C.z
                                        ^^^
Error: Unbound value C.y

I would like to make it work by keeping Extend_with_y and Extend_with_z independent. Extend_with_z would have a signature like (M : sig include X + other things end) -> sig include X val z : int + those other things end, so that it reexports what other types or values of M instead of erasing them and only keeping x and z.

Is it possible? Would it even make sense for OCaml to provide such a behavior? It’s possible that I’m missing something completely obvious, since I haven’t really used functors extensively during the last few months.

1 Like

I don’t think functors would be the right choice here.
What about :

module A : X = 
  let x = 1
end

module With_z = struct
  let z = 3
end

module With_y = struct
  let y = 2
end

module C = struct
   include A
   include With_y
   include With_z
end

Now one reason you might want a functor could be that the value you’re adding to the module depends on what’s inside the module, but it still works

module With_y (A: X) = 
   let y = f a (* f is whatever operation you want *)
end

module C = struct 
   include A
   include With_y(A)
end

I agree it’s a bit more verbose, but to my knowledge (which is not exhaustive on OCaml), there is no way to do this with functors

I indeed need the functors, for reasons not shown in the example. In reality, I’m extending something like Set.Make but showing that in the example would have made it too complex.

I believe the second solution would work. To come back to my example, I could write:

module C = struct
  include Extend_with_y(A)
  include Extend_with_z(A)
end

And if I simplify the functors to only export what they create, I just have to prepend include A in the definition of C. Sounds like a good solution to me.

1 Like

I was thinking about how to implement this and came up with the following code

module type MTy = 
sig 
  module type T 
end

module Extend_x(Ty : MTy)(M : Ty.T) =
struct
  include M
      
  let x = 2
end
                                      

module Extend_y(Ty : MTy)(M : Ty.T) =
struct
  include M
      
  let y = 3
end

module Base = 
struct 
  let a = 1
end

module M1 = Extend_x(struct module type T = module type of Base end)(Base)
module M2 = Extend_y(struct module type T = module type of M1 end)(M1)
    
let result = M2.a + M2.x + M2.y

I was somewhat surprised to discover that this code, which I thought would be valid, actually fails to compile since OCaml seems to demand that include directives must only refer to modules whose module type is known (i.e. not a module type variable). This seems like a big restriction and I was wondering whether it’s in place because of fundamental limitations or simply because there’s no demand for it.

1 Like

Rather than copying the whole module every time (and yes include is a copy) another option is to implement extensions that only add the new features and compose those extensions:

module type X = sig val x : int end

module Extend_with_y(M : X) = struct
  let y = 2
end

module Extend_with_z(M : X) = struct
  let z = 3
end

module A = struct
  let x = 1
end
module B = struct
  include A
  include Extend_with_y(A)
end
module C = struct
  include B
  include Extend_with_z(B)
end
let () =
  Printf.printf "%d, %d, %d\n" C.x C.y C.z

Including abstract module type does not work because include is not part of the module type system: it merely inlines the content of the signature. Being able to include any module with a statistically unknown number of elements would break the compilation of functors. It is also unclear what should happen in this situation in term of scope or binding in

let x = 1
include M
let y = x + 1

if it is not known if M contains x or not.

Similarly, depending on TX and TY

module type S = sig
  include TX
  include TY
end

is not always valid.

1 Like

Thanks for the clarification. I think I see why implementing what I was initially suggesting would cause issues.