4.08 local module aliases: wrap in 'open struct'?

Say I want to refer to a module Foo.Bar.Baz as a shorter name, perhaps B, in a module. I don’t want that module alias to result in any codegen; it’s literally just an alias.

In OCaml 4.08, is there any difference between doing this:

module B = Foo.Bar.Baz

let foo (bar : B.t) = (* ... *)

and this:

open struct
    module B = Foo.Bar.Baz
end

let foo (bar : B.t) = (* ... *)

when B isn’t referenced in the outer module’s interface, or is, perhaps, one of the new forms module B := Foo.Bar.Baz. (Sidenote: it seems strange that the syntax isn’t parallel on both sides, but since Jane Street’s original proposal was then I presume there are good reasons for it not to be.)

I’m wondering if the former results in some sort of code to allow B to be referenced from outside the module, but the latter doesn’t. However, since B isn’t in the interface, it might be that the OCaml compiler just ‘does the right thing’ in both situations. I’m not sure.

Any other suggestions as to better ways of doing this welcome!

There is a small difference because aliases are type-level component. Thus

module B = Foo.Bar.Baz

has no effect on the runtime representation of the module.
Contrarily,

open struct
    module B = Foo.Bar.Baz
end

creates an empty anonymous module that is optimized away at at later stage.

3 Likes

TL;DR; you can use this construct without fearing that it will bloat your code. It will never copy the module contents or create any heavy constructs. With the optimizing compiler no code will be created at all. With the bytecode one extra instruction creating an empty block will be emitted with a hardly noticeable runtime effect.

Semantically, every occurrence of struct end creates a module. For example, the following program,

open struct
  module L = Stdlib.List
end
let foo (bar : _ L.t) = L.length bar

will yield the following lambda code (could be obtained with ocaml -dlambda -c example.ml)

(let
    (open/141 = (makeblock 0)
     foo/82 =
       (function bar/139 : int
         (apply (field 0 (global Stdlib__list!)) bar/139)))
    (makeblock 0 foo/82)))

Notice the open/141 = (makeblock 0) statement, which will create an empty block. Notice also, that this block is never referenced, thanks to the simplification pass which is applied even in the non-optimizing version of the compiler.

If we’re talking about non-optimizing compiler, this would be indeed the code, that will be executed by the VM, i.e., it will have some runtime cost, here is the actual bytecode,

        branch L2
L1:     acc 0
        push
        getglobal Stdlib__list!
        getfield 0
        appterm 1, 2
L2:     makeblock 0, 0
        push
        closure L1, 0
        push
        acc 0
        makeblock 1, 0
        pop 2
        setglobal Example!

we start with L2 as the entry point and the first instruction is to create an empty block. The block creation will be evaluated at the program startup time (or during dynamic linking, if the compilation unit is loaded dynamically).

If we will put a generalized open statement inside a function, it will be evaluated every time a function is applied, e.g., the following code

let bar x =
  let open struct
    module L = Stdlib.List
  end in
  L.length x

will produce the following lambda


(let
    (bar/80 =
       (function x/81 : int
         (let (open/141 = (makeblock 0))
           (apply (field 0 (global Stdlib__list!)) x/81))))
    (makeblock 0 bar/80))

and bytecode

        branch L2
L1:     makeblock 0, 0
        push
        acc 1
        push
        getglobal Stdlib__list!
        getfield 0
        appterm 1, 3
L2:     closure L1, 0
        push
        acc 0
        makeblock 1, 0
        pop 1
        setglobal Example!

notice that makeblock is in the body of the function (which starts with L1 in the bytecode).

Of course, if we will use an optimizing compiler these bogus allocations will be removed, e.g.,

ocamlopt -dclambda -c example.ml 

clambda:
(seq
  (let
    (bar/80
       (closure 
         (fun camlExample__bar_80:int 1  x/81
           (if x/81
             (apply* camlStdlib__list__length_aux_83  1 (field 1 x/81)) 0)) ))
    (setfield_ptr(root-init) 0 (global camlExample!) bar/80))
  0a)

Here, not only the allocation is removed, but also the code was inlined.

To summarize. No code should be generated if an optimizing compiler is used. In the non-optimized bytecode version, a piece of code creating a module value will be yielded by the compiler.

2 Likes