Reputation: 4187
In OCaml, to bring another module in scope you can use open
. But what about code like this:
module A = struct
include B.C
module D = B.E
end
Does this create an entirely new module called A
that has nothing to do with the modules created by B
? Or are the types in B
equivalent to this new structure and can a type in A.t
can be used interchangeably with a type in B.C.t
for example?
Especially, comparing to Rust I believe this is very different from writing something like
pub mod a {
pub use b::c::*;
pub use b::e as d;
}
Upvotes: 1
Views: 992
Reputation: 35210
Yes, module A = struct include B.C end
creates an entirely new module and exports all definitions from B.C
. All abstract types and data types that are imported from B.C
are explicitly related to that module.
In other words, suppose you have
module Inner = struct
type imp = Foo
type t = int
end
so when we import Inner
we can access the Inner
definitions,
module A = struct
include Inner
let x : imp = Foo
let 1 : t = 1
end
and the Foo
constructor in A
belongs to the same type as the Foo
constructor in the Inner
module so that the following typechecks,
A.x = Inner.Foo
In other words, include
is not a mere copy-paste, but something like this,
module A = struct
(* include Inner expands to *)
type imp = Inner.imp = Foo
type t = Inner.t = int
end
This operation of preserving type equalities is formally called strengthening and always applied when OCaml infers module type. In other words, the type system never forgets the type sharing constraints and the only way to remove them is to explicitly specify the module type that doesn't expose the sharing constraints (or use the module type of
construct, see below).
For example, if we will define a module type
module type S = sig
type imp = Foo
type t = int
end
then
module A = struct
include (Inner : S)
end
will generate a new type foo
, so A.Foo = Inner.Foo
will no longer type check. The same could be achieved with the module type of
construct that explicitly disables module type strengthening,
module A = struct
include (Inner : module type of Inner)
end
will again produce A.Foo
that is distinct from Inner.Foo
. Note that type t
will be still compatible in all implementation as it is a manifest type and A.t
is equal to Inner.t
not via a sharing constraint but since both are equal to int
.
Now, you might probably have the question, what is the difference between,
module A = Inner
and
module A = struct include Inner end
The answer is simple. Semantically they are equivalent. Moreover, the former is not a module alias as you might think. Both are module definitions. And both will define a new module A
with exactly the same module type.
A module alias is a feature that exists on the (module) type level, i.e., in the signatures, e.g.,
module Main : sig
module A = Inner (* now this is the module alias *)
end = struct
module A = Inner
end
So what the module alias is saying, on the module level, is that A
is not only has the same type as Inner
but it is exactly the Inner
module. Which opens to the compiler and toolchain a few opportunities. For example, the compiler may eschew module copying as well as enable module packing.
But all this has nothing to do with the observed semantics and especially with the typing. If we will forget about the explicit equality (that is again used mostly for more optimal module packing, e.g., in dune) then the following definition of the module A
module Main = struct
module A = Inner
end
is exactly the same as the above that was using the module aliasing. Anything that was typed with the previous definition will be typed with the new definition (modulo module type aliases). It is as strong. And the following is as strong,
module Main = struct
module A = struct include Inner end
end
and even the following,
module Main : sig
module A : sig
type imp = Impl.imp = Foo
type t = Impl.t = int
end
end = struct
module A = Impl
end
Upvotes: 5