module type Z = sig
type in_file
type entry
end
module type ContentType = sig
type t
type in_file
val make : in_file -> t
end
module MakeContent(Zip : Z) : (ContentType with type in_file = Zip.in_file) = struct
type in_file = Zip.in_file
type t = unit
let make _in_f = ()
end
let from_archive
(type a)
(module Zip : Z with type in_file = a)
(in_file : a)
(* : ??? (ContentType with type in_file = a).t *)
=
let module Content = MakeContent(Zip) in
Content.make in_file
;;
This doesn’t type check:
This expression has type Content.t
but an expression was expected of type 'a
The type constructor MakeContent(Zip).t would escape its scope
Is there a way out? What should be the correct return type of from_archive?
What are you trying to achieve? Do you want to abstract over file compression formats? You are using modules as first-class values (in from_archive) - maybe there is a simpler design overall for what you would like to do.
module type ContentType = sig
type t
type in_file
val make : in_file -> t
end
With this signature, values from type t can be constructed, but then they become unusable black boxes.
Then the second problem is that the line
let module Content = MakeContent(Zip) in
creates a new type MakeContent(Zip).t which does not have a name outside of the from_archive function.
Such type cannot escape the scope of this function since it would be then unnamed.
The solution is probably to write from_archive inside a functor taking a Z module as an argument:
generally, functors are required if you need to create new types and values associated with those types.
Thanks for the comments. My objective was to abstract over the Zip compression library for portability reasons. I would like to target both native and JavaScript backends. I ended up with something like this:
module type Z = sig
type in_file
type entry
end
module type ContentType = sig
type t
type in_file
val make : in_file -> t
val do_something_with_t : t -> unit
val from_archive : in_file -> t
end
module MakeContent(Zip : Z) : (ContentType with type in_file = Zip.in_file) = struct
type in_file = Zip.in_file
type t = unit
let make _in_f = ()
let do_something_with_t _t = print_string "done"
let from_archive = make
end
(* In executable with a concrete implementation of Zip *)
let () =
let module ZipContent = MakeContent(Zip) in
let content = ZipContent.from_archive (Zip.open_in "archive.zip") in
ZipContent.do_something_with_t content
;;
Just for comparison: a more conventional design would use a module that encapsulates the ZIP-specific access functions as a functor parameter.
module type COMPRESSED = sig
exception Error of string
type t
val fopen: string -> t
val read: t -> string
val close: t -> unit
end
module Make(C : COMPRESSED) = struct
let with_archive ~file f =
let file = C.fopen file in
try
let result = C.read file |> f in
C.close file; result
with
e -> C.close file; raise e
end
Just out of curiosity, isn’t there a way to give a hint to the type checker that this type is the abstract type “t” from the ContentType signature, and as long as you use the return value of from_archive in a way consistent with the ContentType signature, it should be fine? Something along the line of (not valid syntax):
let from_archive
(type a)
(module Zip : Z with type in_file = a)
(in_file : a)
: (module : ContentType with type in_file = a).t
=
let module Content = MakeContent(Zip) in
Content.make in_file
No, fundamentally, types belong to modules, and not module types.
Another way to look at the issue at hand is that the type of the function that you are trying to write is
val from_archive:
(module X: Z with type in_file = 'a) -> 'a -> MakeContent(X).t
The crucial part is that the typeMakeContent(X).t of the returned value depends on the valueX of the argument which is not possible outside of the module language.