We have a functor Make that takes a Base module, and a second S module.
Module Base does not have types and does not affect how Make works. Module S however changes what types do_things works with:
module type Base = sig val v : int end
module type S = sig
type a
val to_a : string -> a
end
module Make (B : Base) (X : S) = struct
let do_things a_to_string s =
let a = X.to_a s in
a_to_string a
end
if we have a function
let make (type a) (module B : Base) (module X : S with type a = a)
(a_to_string : a -> string) =
let module M = Make (B) (X) in
M.do_things a_to_string
we can use it to instantiate Make with multiple types:
module Int_s = struct
type a = int
let to_a = Stdlib.int_of_string
end
module Float_s = struct
type a = float
let to_a = Stdlib.float_of_string
end
module B = struct let v = 1 end
let m1 = make (module B) (module Int_s)
let m2 = make (module B) (module Float_s)
This works, but why pass module B every time, if type a is not dependent on it - we can partially apply it:
let make2 = make (module B)
however this no longer works:
let m11 = make2 (module Int_s)
let m22 = make2 (module Float_s)
after the first m11 call - make2 only works with int
I thought moving the type a specifier could work:
let make (module B : Base) (type a) (module X : S with type a = a)
(a_to_string : a -> string) =
let module M = Make (B) (X) in
M.do_things a_to_string
But this doesn’t seem to make a difference. Is there any way to partially apply Base, but not tie make with the type of the first module S that is passed?
This looks like the value restriction.
Consider this simpler example:
# let f _ignored x = x;;
val f : 'a -> 'b -> 'b = <fun>
# f 0 1;;
- : int = 1
# f 0 true;;
- : bool = true
# let f2 = f 0;;
val f2 : '_weak1 -> '_weak1 = <fun>
# f2 1;;
- : int = 1
# f2 true;;
Error: This expression has type bool but an expression was expected of type
int
The usual solution is eta-expansion (in your case, let make2 arg = make (module B) arg).
You can find more about the value restriction in the relevant section of the manual.
The tl;dr (of the manual section) that glosses over semantic reasoning but is a good rule of thumb:
you can’t have “polymorphic values”, even if said values are partial-applications of functions.
This is value syntax:
let make2 = make (module B)
This is function syntax:
let make2 = fun m -> make (module B) m
The rule of thumb is purely syntactic. Function syntax is one of:
let f ... = ...
let f = fun ... -> ...
let f = function ... -> ...
You really should read the manual section for details, reasoning, semantics, etc…
Take note that make is evaluated in the first example but not the second, this is a key detail.
thanks! this was a helpful read, I ended up defining the function inside of a functor, so that Base can be partially applied, and then passing the resulting make through other functors:
module Make (D: sig module B: Base end) = struct let make (module X:...) end
let make2 arg = make (module B) arg) didn’t work well for my use case, because in my real case I had to do something like
let make2 (type a b c) (module X : S with type a = a and type b = b and type c = c) arg
= make (module Base) (module X) arg
Great to know that you’ve solved your problem. Here are a few follow-up questions if you have a bit more time:
I’m a bit surprised that you need something that heavy. First-class modules can be bound to regular variables too:
# module type T = sig type t val x : t end;;
module type T = sig type t val x : t end
# let mk (type a b) (module X : T with type t = a) (module Y : T with type t = b) f = f X.x Y.x;;
val mk :
(module T with type t = 'a) ->
(module T with type t = 'b) -> ('a -> 'b -> 'c) -> 'c = <fun>
# module I = struct type t = int let x = 0 end;;
module I : sig type t = int val x : int end
# let mk2 arg = mk (module I) arg;;
val mk2 : (module T with type t = 'a) -> (I.t -> 'a -> 'b) -> 'b = <fun>
The arg variable in the definition of mk2 is a first-class module, and the type inferred is the same as if local types were used.
Also, I’m not sure why your functor takes as argument (D: sig module B: Base end) instead of just (B : Base); although if you need more than just one module in the functor parameter it makes sense to wrap all of that.
this works perfectly…
I am used to typing types everywhere, and didn’t realize that I can just let OCaml infer the modules and arguments types.
Thank you!
in the contrived example above:
let make3 a b = make (module B) a b
let m111 = make3 (module Int_s)
let m222 = make3 (module Float_s)