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.