Type constructor would escape its scope... How to express return type?

I have the following situation:

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.

Your problem starts here:

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.

1 Like

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
;;
1 Like

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
2 Likes

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 type MakeContent(X).t of the returned value depends on the value X of the argument which is not possible outside of the module language.

3 Likes