4

I've got a complicated set of constraints (mostly pedagogical) that result in me wanting to do something like this:

type alpha = ... ENV.env ...
and module type ENV = sig
    type env
    val foo : ...alpha...
end

But that's not legal OCaml. For one, you can't have module type ENV = as part of a recursive type definition. (I don't think there's even any version of recursive declarations limited just to module type declarations.) For two, you can't have ENV.env invoking a component of a module type; you can only write M.env where M is an implemented module structure. But if something like the above were legal, it would capture what I'm aiming to do.

Here's a simplified test case that exhibits some of my constraints.

(* M1 is really the contents of an .mli file, not a module *)
module M1 = struct
  type env (* I want env to be opaque or abstract at this point ... *)
  type alpha = A of int | B of env * int
  type beta = C of int | D of alpha
  module type ENV = sig
    type env (* ...but to be unified with this env *)
    val foo : unit -> beta -> unit
    val empty : env
  end
end

(* M2 needs to be in another_file.ml *)
module M2 = struct
  (* here we provide an implementation of M1.ENV *)
  module E1 : M1.ENV = struct
    type env = char
    let foo () _ = ()
    let empty = 'X'
  end

  (* then I'd want this to typecheck, but it doesn't *)
  let _ = M1.B(E1.empty, 0)
end

In M1, the part before the declaration of ENV needs to refer to the env type, but then the declaration of ENV itself needs to refer to some of what occurs in the other part of M1. So it's not clear which should come first. If ENV didn't need to refer to beta, which in turn refers to alpha, I could put ENV at the start of the file and include ENV (as I say above, this is really an .mli file) in order to have access to the env type for the declaration of alpha. I'm not sure if that would really result in the env in alpha being the same as the env in M1.ENV (in OCaml include-ing a module type is said to be just a textual copy of its contents); but in any case I can't do it here because of the interdependencies. So I have to pre-declare type env at the start of M1. It's essential to my needs that we're not in a position in M1 to specify an implementation for env.

The declaration I gave above for M1 is accepted by OCaml, but the two types env aren't unified. That's not surprising, but my task is to find some contortions that will unify them. When we provide an implementation for ENV later, as in M2 above, we want to be able to use it to provide instances of M1.alpha. But currently we're not: M1.B(E1.empty, 0) won't typecheck.

Now there is one solution. I could have M1.alpha use a type variable 'env rather than an abstract type env. But then M1.alpha would need to be parameterized on 'env, and then so too would M1.beta be, and because of the interdependencies in my types, just about every type in the whole project then needs to carry a parameterization on the 'env type, a concrete instance of which we're not able to supply until we get to module M2, further down in the buildchain. This is pedagogically undesirable, as it makes all the types harder to understand, even in contexts where the environments have no immediate relevance.

So I've been trying to figure out if there's some trick I could perform with functors or with first-class modules that would enable me to get the kinds of interdependencies I'm looking for in module M1, and provide an implementation of the env type in a later file, here represented by module M2. I haven't been able to figure such a thing out yet.

dubiousjim
  • 4,722
  • 1
  • 36
  • 34

2 Answers2

4

I don't know if this is helpful at all, but this tiny example works for me:

# module rec A : sig
    type alpha = B.env list
  end = A    
  and B : sig
    type env
    val foo: A.alpha
  end = struct
    type env = int
    let foo = [3]
  end;;
module rec A : sig type alpha = B.env list end
and B : sig type env val foo : A.alpha end
# B.foo
- : A.alpha = [<abstr>]

It seems to have a structure reminiscent of your initial example, with the restriction that alpha ends up wrapped in a module.

mndrix
  • 3,131
  • 1
  • 30
  • 23
Jeffrey Scofield
  • 65,646
  • 2
  • 72
  • 108
  • Thanks, yes using recursive modules is an option for the toy example I gave, but not in reality. One reason being that these live in different files. The other reason being that M1 is really the body of an .mli file, so it's a module type not a module. But in my toy example I couldn't see how to declare M1 as a module type and yet still refer to its components in M2. But you can do that to compiled .mli files. – dubiousjim Mar 18 '15 at 06:39
  • A true module expert would be more helpful than I can be. But maybe you can define your recursive types in one place, then call them out in your `.mli` file? I've done this in the past (with simpler structures). – Jeffrey Scofield Mar 18 '15 at 06:51
  • But your answer did teach me that you can do `module rec A : sig ... end = A`, when the only contents you want `A` to have are types. This is interesting and seems useful. Thanks. – dubiousjim Mar 18 '15 at 06:52
2

As I said in a comment, @Jeffrey-Scofield's answer revealed to me the nice trick of using recursive module definitions to repeat the type-only part of a module sig in the module implementation, without needing to repeat it. This and a bit of thinking gave me the following solution to my test case. It's being a solution turns on my having some flexibility about where in the buildchain M2 gets positioned, and also my being willing to make M1.ENV be an extension of the rest of the signature of M1, and have other files in the package use the implementation of that provided by M2 instead of using M1. These are all compatible with my real constraints.

The trick is to do this:

(* M1 is really the contents of an .mli file, not a module *)
module M1 = struct
  (* we encapsulate the prefix of M1 in its own sig *)
  module type Virtual = sig
    type env2 (* an abstract type for now ... *)
    type alpha = A of int | B of env2 * int
    type beta = C of int | D of alpha
  end
  module type ENV = sig
    type env
    (* Now we include Virtual inside ENV, unifying their types
       using the standard OCaml method. This makes ENV an
       extension of the other parts of M1, rather than a small
       standalone sig. But that's OK; see below. *)
    include Virtual with type env2 = env
    (* now beta is available *)
    val foo : unit -> beta -> unit
    val empty : env
  end
end (* M1 *)

(* M2 needs to be in another_file.ml *)
module M2 = struct
  (* here we provide an implementation of E1 *)
  module E1 : M1.ENV = struct
    type env = char
    let foo () _ = ()
    let empty = 'X'
    (* Here's how we can easily provide all the rest of ENV
       that we're now obliged to provide. *)
    module rec MX : M1.Virtual with type env2 = env = MX
    include MX
  end
  (* this should be legitimate, and it is! *)
  let _ = E1.B(E1.empty, 0)
end (* M2 *)

EDIT: In my real use-case, I was wanting to have the implementations of type env use other types from M1.Virtual. I ended up needing to do something like this:

  module E1 : M1.ENV = struct
   module type TMP = sig
      type tmp_beta (* or some other type from Virtual, which we can't include until after declaring env *)
      type env = int -> tmp_beta
      include M1.Virtual with type env2 = env
    end
    (* now we unify tmp_beta with Virtual.beta *)
    module rec TMP : TMP with type tmp_beta = TMP.beta = TMP
    include TMP
  end (* E1 *)

That's an awful lot of contorting. But it seems to work. Have added this technique here in case others might also have a need to "forward-declare" types in OCaml, but are prevented from doing so via the usual recursive type declarations for some reason --- as I was by the need to cross a module type = barrier.

dubiousjim
  • 4,722
  • 1
  • 36
  • 34