-
Notifications
You must be signed in to change notification settings - Fork 234
Modules, namespaces and name resolution
F* has had modules and namespaces since its inception. Pull request 772 introduces a number of related improvements, which this article explains in extenso.
- Modules
open
- Shadowing and local
open
- Local
open
and interleaving - Namespaces
- Module abbreviations
include
- Summary: name resolution
Similarly to OCaml, a F* module is a collection of top-level definitions of types, terms (and also effects), introduced by a module name:
module ModuleA
let term = 18
type typ = | Datacon1 | Datacon2
Similarly to OCaml, the name of a module must start with an uppercase letter.
A module must be stored in a file such that the basename (without the .fst
file name extension) converted in lowercase must match the module name converted in lowercase. In this example, any one of modulea.fst
, ModuleA.fst
, MODULEA.FST
will work.
Now, another module, say ModuleB
, can access the definitions of ModuleA
by qualifying each definition with the name of the module:
module ModuleB
let b : bool = true
let example : ModuleA.typ =
if b
then ModuleA.Datacon1
else if ModuleA.term = 42
then ModuleA.Datacon1
else ModuleA.Datacon2
IMPORTANT: Contrary to OCaml, F* has no nested modules. In other words, a module cannot contain other modules. Thus, a F* source file always contains exactly one module.
However, it is not always necessary to manually qualify every access to ModuleA
in ModuleB
. Similarly to OCaml, F* allows users to access definitions of ModuleA
without qualifying them, using the open
keyword. Thus ModuleB
can be equivalently rewritten as ModuleC
below:
module ModuleC
let b : bool = true
open ModuleA
let example : typ =
if b
then Datacon1
else if term = 42
then Datacon1
else Datacon2
In this example, open ModuleA
makes the definitions of ModuleA
reachable from within ModuleC
without being qualified.
NOTE: open
only applies within the module it is declared. For instance, accessing ModuleC
from outside will not have ModuleA
automatically reachable through ModuleC.
:
let example2 = ModuleC.example
let term2 = ModuleC.term (* <-- this will fail. *)
let term3 = ModuleA.term (* <-- this will succeed. *)
Consider the following example:
module K
let z = 18
module L
open K
let z = 42
let example = z
In this example, z
is defined after open K
, so z
shadows K.z
and example
has value 42.
However, consider the following example:
module U
let left = 1
let middle = 2
let right = 3
module V
let left = 4
open U
let example = left + (middle + right)
Here, in fact, example
will have value 1+2+3 instead of 4+2+3, because open U
brings all definitions of U
into the current scope, thus making U.left
shadow V.left
.
It is however possible to open
a module only in the middle of a term rather than globally:
module W
let left = 4
let example = left + let open U in (middle + right)
Equivalently, let open Module in term
has an alternative syntax Module.(term)
also borrowed from OCaml, so that module W
is syntactically equivalent to the following:
module W
let left = 4
let example = left + U.(middle + right)
NOTE: .(
is a token, i.e. no whitespace is allowed in between .
and (
Consider the following example:
module M
let a = 1
let b = 2
module N
let example =
let a = 3 in
let open M in
let b = 4 in
a + b
Here, example
will have value 1+4 because let open M
makes M.a
shadow the locally-defined a
, but since b
is defined after let open M
, it thus shadows M.b
.
A module name can itself be qualified by names (each starting with an upper-case letter), yielding a long module name. For instance:
module A.X
let u = 18
Module A.X
can be defined regardless of whether module A
is defined or not. As before, A.X
must be stored in a file such that the basename converted in lowercase is the same as the whole module name converted in lowercase, such as a.x.fst
, a.X.fst
, A.x.fst
, etc.
TODO: do we have path-defined namespaces à la Coq?
The purpose of modules with long names is to define namespaces that can also be open
ed, this time similary to F#. The following example is thus valid:
module A.X
let u = 18
module A.Y
let v = 42
module B
open A
let example = X.18 + Y.42
In this example, A
is said to be a namespace that is opened to resolve module names.
IMPORTANT: There are several restrictions:
-
similarly to F#, this example no longer works if module
A
is defined. More precisely, ifA
is a module, thenA
is no longer a namespace andopen A
only opens the module, not the namespace. For example:module B let u = 18 module B.X let v = 42 module C open B let example1 = u (* <-- works, resolves to B.u *) let example2 = X.v (* <-- resolves to X.v, not B.X.v, thus fails *)
-
In
open A
, nameA
is taken as is, and does not trigger namespace-based name resolution:module A.X.Y let u = 18 module B open A open X (* <-- opens module/namespace X, not A.X *) let example = Y.u (* resolves to Y.u, not A.X.Y.u, thus fails *)
Long module names can be too long. So, it is possible to define module abbreviations:
module A.Very.Long.Name
let x = 18
module B
module AVLN = A.Very.Long.Name
let example = AVLN.x
There are several restrictions, however:
-
Module abbreviations are only reachable from within the module where they are defined.
module A let x = 18 module B module C = A module D let example = B.C.x (* <-- resolves to B.C.x, not A.x, thus fails *)
This is consistent with the principle that F* has no nested modules.
-
A module abbreviation itself cannot be qualified, so
module A.B = C
is forbidden. -
When defining a module abbreviation, the module name must be fully qualified. In other words,
open
-based module name resolution is not honored on the right-hand-side of a module abbreviation:module A.B let x = 18 module C open A module D = B (* <-- resolves to B, not A.B, so fails *)
-
A module abbreviation only rewrites a module name, not part of a namespace name:
module A let x = 18 module A.U let y = 42 module B module C = A let example = C.U.y (* <-- resolves to C.U.y, not A.U.y, thus fails *)
As an extra feature, open M
works if M
is a module abbrev, such as in the following example:
module A
let x = 18
module B
module C = A
open C (* <-- module abbrev allowed, so actually opens module A *)
let example = x (* <-- works, resolves to A.x *)
It is sometimes desirable to split a library into several distinct modules A1
, A2
, etc. (for instance, when A1
contains abstract definitions and A2
should be based only on A1
's definition types) and still have the end user access all definitions through a single module name A
. To this end, we provide the include
feature.
From within a module, include
behaves like open
except that it is restricted to modules (namespaces are not allowed.)
The main impact of include
is outside of the module it is declared. Consider the following example:
module A
let x : int = 18
module B
include A
let y : bool = true
let k = x (* succeeds, resolves into A.x *)
module C
let z = B.y (* succeeds *)
let t = B.x (* succeeds, resolves into A.x *)
Within B
, include A
behaves like open A
, so x
resolves into A.x
. What includes
offers more is that this resolution exposes x
as if it were defined in B
, so that any occurrence of B.x
resolves into A.x
. (Internally however, the definition of x
in A
is not copied into B
; instead, include
is fully based on name resolution.)
-
include
is sensitive to shadowing of definitions:module U let x = 18 let y = 42 module V include U let y = 1 (* shadows U.y *) module W let z = V.x (* resolves into U.x *) let t = V.y (* resolves into V.y *)
-
include
is sensitive to shadowing byinclude
itself:module R let x = 18 let y = 42 module S let x = 1729 module T include R include S (* shadows R.x *) module U let z = T.y (* succeeds, resolves into R.y *) let t = T.x (* succeeds, resolves into S.x *)
-
include
is transitivemodule K let x = 18 module L include K module M include L module N let y = M.x (* succeeds, resolves into K.x *)
-
include
is compatible withopen
module D let x = 18 module E include D module F open E let y = x (* succeeds, resolves into D.x *)
-
include B
does not expose module abbreviations defined inB
or itsinclude
d ancestors:module A let x = 18 module B module Z = A module C include B module D let y = C.Z.x (* resolves into C.Z.x, not A.x, thus fails *)
include
is now actively used in the standard library:
-
FStar.Seq
is obtained by "merging"FStar.Seq.Base
andFStar.Seq.Properties
throughinclude
- similarly,
FStar.List.Tot
is obtained fromFStar.List.Tot.Base
andFStar.List.Tot.Properties
open A
-
A
must be a fully qualified namespace or module name, or a module abbrev. In other words,open
is not honored to resolveA
itself, butmodule A = ...
is. - If module
A
is defined, thenopen
s the moduleA
only. - Otherwise,
open
s the whole namespaceA
.
module A = B
-
A
must be a short name (not qualified) -
B
must be a fully qualified module name, so neithermodule B = ...
noropen B
are honored to resolveB
itself. - Only rewrites module
A
, not namespace prefixA
.
module A include B
-
B
must be a fully qualified module name, or a module abbrev (not a namespace.) - From within
A
, behaves likeopen B
- From outside of
A
, any term or typex
(but not module abbrev) defined inB
not subsequently shadowed inA
can be referred to asA.x
-
include
is transitive