How to abstract module types?

I don’t get how to abstract module types;

Here’s what I tried - let’s say that we have some module with a function make

module type B = sig
  type t
  type err
  type 'a do_things = t -> ('a, err) result
  
  val make: string do_things
end

we can implement the types and everything works as expected - val make : string do_things

module My_b: B = struct
 type t = int
 type err = string
 type 'a do_things = t -> ('a, err) result
 
 let make x =
   print_endline (string_of_int x);
   Ok "ok"
end

but these 3 types - t, err and do_things are used in multiple modules, so we want to define them in a share module type, instead of re-defining them

module type A = sig
 type t
 type err
 type 'a do_things = t -> ('a, err) result
end

now we can define the same module, but with the types moved:

module type C = sig
  module A: A
  
  val make: string A.do_things
end

and as before - the implementation works

module My_c: C = struct
  module A = struct
   type t = int
   type err = string
   type 'a do_things = t -> ('a, err) result
  end
 
 let make x =
   print_endline (string_of_int x);
   Ok "ok"
end

but since we have a common implementation of A, we define it once and reuse it:

module My_a: A = struct
 type t = int
 type err = string
 type 'a do_things = t -> ('a, err) result
end

let’s try the implementation again

module My_c2: C = struct
 module A = My_a

 let make x =
   print_endline (string_of_int x);
   Ok "ok"
end

error - signature mismatch, but I don’t understand - My_a is the same as the defined types above?

Values do not match:
  val make_something : int -> (string, 'a) result
is not included in
  val make_something : string A.do_things

how to make this work? how can I define an abstract module type and reuse it?

2 Likes

Hi @mudrz,

Your problem is in the following line:

module My_a: A = struct

The module type annotation : A has the effect of abstracting away any details of the My_a definition that aren’t described by A. In this case, this has the effect of making the types My_a.t and My_a.err abstract everywhere but inside its own definition. You then try to define My_c2 which requires that My_a has particular concrete types t = int and err = string, but the type-checker complains that this is not necessarily the case (because the : A has hidden this information).

The solution is to either drop this module type annotation, or to add type equality constraints to it that preserve the information you need elsewhere:

module My_a = struct ... end
module My_a : A with type t = int and type err = string = struct ... end

Hope this helps!

5 Likes

Another important point is that module types never define anything, there are specifications: they provide a restricted view on some modules. Thus in

module type A = sig
 type t
 type err
 type 'a do_things = t -> ('a, err) result
end

you are not defining a common type t, but you are a defining a view on a module that merely allows the user of the view to known that the restricted module contain a type ŧ and nothing more.

3 Likes

wow nice, that’s exactly what I was looking for, thank you (I still want My_a to implement A, because it is complex)

I wish I didn’t have to repeat the types in multiple places, but it solves the problem

thanks, that’s really helpful, I never though to think about module types as restricted views

Note that you can also give a name to the specialised A:

module type A_int_string = A with type t = int and type err = string
module My_a : A_int_string = struct ... end

This may help to avoid some repetitions.

2 Likes

You can also define the types in a module and define the module type using it:

module A = struct
 type t
 type err
 type 'a do_things = t -> ('a, err) result
end

module type A_sig = module type of A
3 Likes

this relates to the same problem, so adding it in the same thread

If

  • a module has multiple dependencies (in our case My_z depends on X and Y)
  • these dependencies depend on / contain a module A to provide the type
  • the type needs to be consistent across the provided modules

so for example:

module type A = sig
  type t
end
module type X = sig
  module A: A
end
module type Y = sig
  module A: A
  val get: unit -> A.t
end
module type Z = sig
  module A: A
  val get2: unit -> A.t
end

In this case My_z would result in a type error, because Y does not have the same module A as X:

module My_z (X: X) (Y: Y) : Z = struct
  module A = X.A
  let get2 () = Y.get ()
end

we can constrain Y to have the same module A and this would work:

module My_z (X: X) (Y: Y with module A = X.A) : Z = struct
  module A = X.A
  let get2 () = Y.get ()
end

meaning that My_z can be initialized with

module My_a: A with type t = int = struct type t = int end
module My_x: X with module A = My_a = struct
  module A = My_a
end
module My_y: Y with module A = My_a = struct
  module A = My_a
  let get () = 99
end

module My_z2 = My_z (My_x) (My_y)

or we can even limit the injected modules with a concrete module A

module My_z2 (X: X with module A = My_a) (Y: Y with module A = My_a) : Z = struct
  module A = X.A
  let get2 () = Y.get () + 1
end

is this the sensible way to constrain functors or is there a better way?
(I am looking for a solution to solving dependency injection and class generics from other languages)

Hey folks. Coming from ReasonML/JS land, I imagine this pattern is available to be used in some frontend context. Any of you have any idea what that might look like? Just a general question. Every time I see and idiomatic OCaml functor, I wonder how I might be able to use it. Merci.