Value resolution in recursive modules

Hey,

I’m wondering why this works as expected:

module rec PA : sig
  val print: int array -> unit
end = struct
  let print =
    let iter = Array.iter P.print in
    fun a -> iter a
end
and P : sig
  val print: int -> unit
end = struct
  let print i =
    print_endline (string_of_int i)
end
    
let () =
  PA.print [|1; 2|]

But this crashes (on PA.u):

module rec PA : sig
  val u : int -> unit
  val print: int array -> unit
end = struct
  let u = P.print 
  let print =
    let iter = Array.iter P.print in
    fun a -> iter a
end
and P : sig
  val print: int -> unit
end = struct
  let print i =
    print_endline (string_of_int i)
end

let () = PA.u 3

Notably; in both cases P.print is referenced to outside of a function body. Why is the let binding lazily evaluated in the first case and eagerly in the latter? Is there a doc on how the value resolution in recursive modules work?

Recursive module feature is still marked as an experimental extension of OCaml.
You can find the documentation at https://caml.inria.fr/pub/docs/manual-ocaml/manual024.html

For recursive modules, compiler requires that all dependency cycles between the recursively-defined module identifiers go through at least one “safe” module. A safe module is a module where all values are of the type type expression -> type expression'

In this example. P.u is at compile type substituted with ‘fun _ -> raise Undefined_recursive_module’, and since the value is not on the type type expression -> type expression', its stays like this.

You should change U to be unit -> int -> unit, and the implementation to be let u () = P.print. The it should work.

I don’t understand what it means for a value to be of the type type expression -> type expression'. Could you clarify?

It basically means that all exposed bindings should be functions.
You can find the definition of type expressions here: https://caml.inria.fr/pub/docs/manual-ocaml/types.html#typexpr

The way recursive modules are compiled make your first example, which sounds dangerous, actually ok: the P.print function is unsafe when it is stored in the iter closure, but becomes initialized later and when you call PA.print it works as expected.

However, when initializing u, P.print is still not initialized yet, but its block (not its pointer) is copied into u, and even when P.print is initialized later u is not changed.

Defining u as fun x -> P.print x would force an additional indirection, and be safe.

But basically, even your first example should not be assumed to work, in my opinion.

Also, due to some internal changes in the implementation of recursive modules, your second example will actually work in 4.09.0, but only in bytecode

This is quite amusing. Look:

module rec PA : sig
  val u: int -> unit
  val print: int array -> unit
end = struct
  let u = P.print
  let print =
    fun a -> Array.iter u a
end
and P : sig
  val print: int -> unit
end = struct
  let print i =
    print_endline (string_of_int i)
end
    
let () =
  PA.print [|1; 2|]

let () =
  PA.u 1

This sample… throws on the second call to PA.u so apparently something funky is going on there. Can it be that some optimisation performed by the compiler accidentally causes u to be resolved correctly?

I’m using a tool which generates code like this and it works but feels kind of fragile. Do you think it’s wise to change it so that the top level (structure item) let binding is a function?

There are two different values for u:

  • the value created by the let-binding, which is an alias to P.print.
    This value is unsafe to use until P has been initialized, but becomes safe later.
  • the field of the PA module.
    It is of course unsafe until PA is initialized, but its initialization replaces its contents by the current contents of the let-bound value, and this does not get patched again later.

The definition of PA.print captures the former one, which is why it works.
I would assume that if you define P before PA the use of PA.u would start working too.

If you can ensure that everything toplevel is a function, you will get much more reliable behaviour, so I would advise to make the change.

Thank you @fugmann and @vlaviron. It clears things up a lot.