Namespaces and Modules

Namespaces and Modules

Clef is primarily an expression-based language. Source code units are made up of declarations, some of which can contain further declarations. Declarations are grouped using namespace declaration groups, type definitions, and module definitions. A file may contain multiple namespace declaration groups; types and modules may contain member, function, and value definitions, which contain expressions.

Declaration elements are processed in the context of an environment. The definition of the elements of an environment is found in § Name Resolution.

NORMATIVE: Modules and namespaces in Clef organize names, not compilation. Mutual references between modules — and between types or values within modules — are resolved by CCS’s syntactic dependency analysis (see § Compilation Pipeline), not by recursion keywords. Clef has no let rec, no and for grouping mutual definitions, no module rec, and no namespace rec. The [<AutoOpen>] attribute is not part of Clef; module re-exports are explicit. See § for the attribute set.

namespace-decl-group :=
    namespace long-ident module-elems       -- elements within a namespace
    namespace global module-elems           -- elements within no namespace

module-defn :=
    attributesopt module accessopt ident = module-defn-body

module-defn-body :=
    begin module-elemsopt end

module-elem :=
    module-function-or-value-defn           -- function or value definitions
    type-defns                              -- type definitions
    exception-defn                          -- exception definitions
    module-defn                             -- module definitions
    module-abbrev                           -- module abbreviations
    import-decl                             -- import declarations
    compiler-directive-decl                 -- compiler directives

module-function-or-value-defn :=
    attributesopt let function-defn
    attributesopt let value-defn
    attributesopt do expr

import-decl := open long-ident

module-abbrev := module ident = long-ident

compiler-directive-decl := # ident string ... string

module-elems := module-elem ... module-elem

access :=
    private
    internal
    public

Namespace Declaration Groups

Modules and types in an F# program are organized into namespaces, which encompass the identifiers that are defined in the modules and types. New components may contribute entities to existing namespaces. Each such contribution to a namespace is called a namespace declaration group.

In the following example, the MyCompany.MyLibrary namespace contains Values and x:

namespace MyCompany.MyLibrary

    module Values1 =
        let x = 1

A namespace declaration group is the basic declaration unit within an F# implementation file and is of the form

namespace long-ident

module-elems

The long-ident must be fully qualified. Each such group contains a series of module and type definitions that contribute to the indicated namespace. An implementation file may contain multiple namespace declaration groups, as in this example:

namespace MyCompany.MyOtherLibrary

    type MyType() =
        let x = 1
            member v.P = x + 2

    module MyInnerModule =
        let myValue = 1

namespace MyCompany.MyOtherLibrary.Collections

    type MyCollection(x : int) =
        member v.P = x

Namespace declaration groups may not be nested.

A namespace declaration group can contain type and module definitions, but not function or value definitions. For example:

namespace MyCompany.MyLibrary

// A type definition in a namespace
type MyType() =
    let x = 1
    member v.P = x+2

// A module definition in a namespace
    module MyInnerModule =
        let myValue = 1

// The following is not allowed: value definitions are not allowed in namespaces
let addOne x = x + 1

When a namespace declaration group N is checked in an environment env, the individual declarations are checked and an overall namespace declaration group signature Nsig is inferred. An entry for N is added to the ModulesAndNamespaces table in the environment env (see § Opening Modules and Namespace Declaration Groups).

NORMATIVE: Namespace declaration groups SHALL be processed in dependency order, computed by CCS from the syntactic export map (see § Compilation Pipeline). Mutual references between namespace declaration groups are supported without ceremony.

The following code is well-formed in Clef regardless of the textual order of the two namespaces:

namespace Utilities.Part1

module Module1 =
    let x = Utilities.Part2.Module2.x + 1

namespace Utilities.Part2

module Module2 =
    let x = Utilities.Part1.Module1.x - 1

CCS detects the mutual reference, elaborates the two namespaces as a single dependency group, and exposes the strongly-connected components of the file-level reference graph through its public API. A team policy or build configuration MAY require all cycles to be reviewed; the policy enforcement is a tool concern, not a language concern.

All contributions to a namespace are in mutual scope. A module Values2 = in namespace MyCompany.MyLibrary may reference Values1 declared in another namespace MyCompany.MyLibrary block — in the same file or a different file, in any source order:

namespace MyCompany.MyLibrary

module Values1 =
    let x = 1

namespace MyCompany.MyLibrary

module Values2 =
    let x = Values1.x

Module Definitions

A module definition is a named collection of declarations such as values, types, and function values. Grouping code in modules helps keep related code together and helps avoid name conflicts in your program. For example:

module MyModule =
    let x = 1
    type Foo = A | B
    module MyNestedModule =
        let f y = y + 1
        type Bar = C | D

When a module definition M is checked in an environment env0, the individual declarations are checked in order and an overall module signature Msig is inferred for the module. An entry for M is then added to the ModulesAndNamespaces table to environment env0 to form the new environment used for checking subsequent modules.

Module definitions, like namespace declaration groups, are processed in dependency order. Mutual references between modules are supported without recursion keywords.

The following code is well-formed in Clef regardless of textual module order:

module Part1 =

    let x = Part2.StorageCache()

module Part2 =

    type StorageCache() =
        member cache.Clear() = ()

No two types or modules may have identical names in the same namespace. If a type and a module in the same assembly and namespace are given the same name, the compiler will automatically append Module as a suffix to the compiled name. The [<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>] attribute can be used to cause a module name to always be appended with Module.

type Cat(kind: string) =
    member x.Meow() = printfn "meow"
    member x.Purr() = printfn "purr"
    member x.Kind = kind

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Cat = // Cat is explicitly compiled as CatModule

    let tabby = Cat "Tabby"
    let purr (c:Cat) = c.Purr(); c
    let purrTwice (c:Cat) = purr (purr c)

Cat.tabby |> Cat.purr |> Cat.purrTwice

type Dog(kind: string) =
    member x.Bark() = printfn "woof"
    member x.Wag() = printfn "wag"
    member x.Kind = kind

module Dog = // Dog is implicitly compiled as DogModule

    let beagle = Dog "Beagle"
    let bark (d:Dog) = d.Bark(); d
    let barkTwice (d:Dog) = bark (bark d)

Dog.beagle |> Dog.bark |> Dog.barkTwice

type Cage<'Pet>(pet: 'Pet) =
    member x.Pet = pet

module Cage = // Cage is compiled as Cage because Cage<'Pet> is a generic type so the name does not conflict
    let putInCage pet = Cage pet
    let getPet (cage: Cage<'Pet>) = cage.Pet

let catCage = Cage.putInCage Cat.tabby
let dogCage = Cage.putInCage Dog.beagle

catCage |> Cage.getPet |> Cat.purr
dogCage |> Cage.getPet |> Dog.bark

Function and Value Definitions in Modules

Function and value definitionsin modules introduce named values and functions.

let function-or-value-defn

The following example defines value x and functions id and fib. Recursion is implicit; CCS detects the self-reference in fib from its body:

module M =
    let x = 1
    let id x = x
    let fib x = if x <= 2 then 1 else fib (x - 1) + fib (x - 2)

Function and value definitions in modules may declare explicit type variables and type constraints:

let pair<'T>(x : 'T) = (x, x)
let dispose<'T when 'T :> IDisposable>(x : 'T) = x.Dispose()
let convert<'T, 'U>(x) = unbox<'U>(box<'T>(x))

A value definition that has explicit type variables is called a type function (§ Type Function Definitions in Modules).

Function and value definitions may specify attributes:

// A value definition with the Obsolete attribute
[<Obsolete("Don't use this")>]
let oneTwoPair = ( 1 , 2 )

// A function definition with an attribute
[<Obsolete("Don't use this either")>]
let pear v = (v, v)

By the use of pattern matching, a value definition can define more than one value. In such cases, the attributes apply to each value.

// A value definition that defines two values, each with an attribute
[<Obsolete("Don't use this")>]
let (a, b) = (1, 2)

Values may be declared mutable:

// A value definition that defines a mutable value
let mutable count = 1
let freshName() = (count <- count + 1; count)

Function and value definitions in modules are processed in the same way as function and value definitions in expressions (§ Checking and Elaborating Function Value and Member Definitions), with the following adjustments:

  • Each defined value may have an accessibility annotation (§ Accessibility Annotations). By default, the accessibility annotation of a function or value definition in a module is public.
  • Each defined value is externally accessible if its accessibility annotation is public and it is not hidden by an explicit signature. Externally accessible values are guaranteed to have compiled representations in the output binary.
  • Each defined value can be used to satisfy the requirements of any signature for the module (§ Signature Conformance).
  • Each defined value is subject to arity analysis (§ Arity Inference).
  • Values may have attributes.

Clef Note: The ThreadStatic and ContextStatic attributes are not available in Clef. Thread-local storage uses platform-specific mechanisms.

Literal Definitions in Modules

Value definitions in modules may have the Literal attribute. This attribute causes the value to be compiled as a constant. For example:

[<Literal>]
let PI = 3.141592654

Literal values may be used in custom attributes and pattern matching. For example:

[<Literal>]
let StartOfWeek = DayOfWeek.Monday

[<MyAttribute(StartOfWeek)>]
let feeling(day) =
    match day with
    | StartOfWeek -> "rough"
    | _ -> "great"

A value that has the Literal attribute is subject to the following restrictions:

  • It may not be marked mutable or inline.

  • It may not also have the ThreadStatic or ContextStatic attributes.

  • The right-hand side expression must be a literal constant expression that is both a valid expression after checking, and is made up of either:

    • A simple constant expression, with the exception of (), native integer literals, unsigned native integer literals, byte array literals, BigInteger literals, and user-defined numeric literals.

      or

  • A reference to another literal

    or

  • A bitwise combination of literal constant expressions

    or

  • A + concatenation of two literal constant expressions which are strings

    or

  • enum x or LanguagePrimitives.EnumOfValue x where x is a literal constant expression.

Type Function Definitions in Modules

Value definitions within modules may have explicit generic parameters. For example, ‘T is a generic parameter to the value empty:

let empty<'T> : (list<'T> * Set<'T>) = ([], Set.empty)

A value that has explicit generic parameters but has arity [] (that is, no explicit function parameters) is called a type function. The following are some example type functions from the F# library:

val sizeof<'T> : int
module Set =
    val empty<'T> : Set<'T>
module Map =
    val empty<'Key,'Value> : Map<'Key,'Value>

Clef Note: The typeof<'T> type function is not available in Clef because there is no runtime type system. Type information is resolved entirely at compile time. Use sizeof<'T> for size queries.

Type functions are rarely used in F# programming, although they are convenient in certain situations. Type functions are typically used for:

  • Pure functions that compute type-specific information based on the supplied type arguments.
  • Pure functions whose result is independent of inferred type arguments, such as empty sets and maps.

Type functions receive special treatment during generalization (§ Generalization) and signature conformance (§ Signature Conformance). They typically have either the RequiresExplicitTypeArguments attribute or the GeneralizableValue attribute. Type functions may not be defined inside types, expressions, or computation expressions.

In general, type functions should be used only for computations that do not have observable side effects. However, type functions may still perform computations. In this example, r is a type function that calculates the number of times it has been called

let mutable count = 1
let r<'T> = (count <- count + 1); ref ([] : 'T list);;
// count = 1
let x1 = r<int>
// count = 2
let x2 = r<int>
// count = 3
let z0 = x1
// count = 3
 

The elaborated form of a type function is that of a function definition that takes one argument of type unit. That is, the elaborated form of

let ident typar-defns = expr

is the same as the compiled form for the following declaration:

let ident typar-defns () = expr

References to type functions are elaborated to invocations of such a function.

Active Pattern Definitions in Modules

A value definition within a module that has an active-pattern-op-name introduces pattern-matching tags into the environment when the module is accessed or opened. For example,

let (|A|B|C|) x = if x < 0 then A elif x = 0 then B else C

introduces pattern tags A, B, and C into the PatItems table in the name resolution environment.

“do” statements in Modules

A do statement within a module has the following form:

do expr

The expression expr is checked with an arbitrary initial type ty. After checking expr, ty is asserted to be equal to unit. If the assertion fails, a warning rather than an error is reported. This warning is suppressed for plain expressions without do in interactive (.clefx) files.

A do statement may have attributes:

let main() =
    Console.WriteLine "Application started"
    0

[<EntryPoint>]
do main() |> ignore

Import Declarations

Namespace declaration groups and module definitions can include import declarations in the following form:

open long-ident

Import declarations make elements of other namespace declaration groups and modules accessible by the use of unqualified names. For example:

open FSharp.Collections
open System

Import declarations can be used in:

  • Module definitions and their signatures.
  • Namespace declaration groups and their signatures.

An import declaration is processed by first resolving the long-ident to one or more namespace declaration groups and/or modules [ F1, …, Fn ] by Name Resolution in Module and Namespace Paths (§ Name Resolution in Module and Namespace Paths). For example, Collections may resolve to one or more namespace declaration groups, one for each source package that contributes a namespace declaration group in the current environment. Next, each Fi is added to the environment successively by using the technique specified in § Opening Modules and Namespace Declaration Groups. An error occurs if any Fi is a module that has the RequireQualifiedAccess attribute.

Module Abbreviations

A module abbreviation defines a local name for a module long identifier, as follows:

module ident = long-ident

For example:

module Ops = FSharp.Core.Operators

Module abbreviations can be used in:

  • Module definitions and their signatures.
  • Namespace declaration groups and their signatures.

Module abbreviations are implicitly private to the module or namespace declaration group in which they appear.

A module abbreviation is processed by first resolving the long-ident to a list of modules by Name Resolution in Module and Namespace Paths (see § Name Resolution). The list is then appended to the set of names that are associated with ident in the ModulesAndNamespaces table.

Module abbreviations may not be used to abbreviate namespaces.

Accessibility Annotations

Accessibilities may be specified on declaration elements in namespace declaration groups and modules, and on members in types. The table lists the accessibilities that can appear in user code:

AccessibilityDescription
publicNo restrictions on access.
privateAccess is permitted only from the enclosing type, module, or namespace declaration group.
internalAccess is permitted only from within the enclosing assembly, or from assemblies whose name is listed using the InternalsVisibleTo attribute in the current assembly.

The default accessibilities are public. Specifically:

  • Function definitions, value definitions, type definitions, and exception definitions in modules are public.
  • Modules, type definitions, and exception definitions in namespaces are public.
  • Members in type definitions are public.

Some function and value definitions may not be given an accessibility and, by their nature, have restricted lexical scope. In particular:

  • Function and value definitions in classes are lexically available only within the class being defined, and only from the point of their definition onward.
  • Module type abbreviations are lexically available only within the module or namespace declaration group being defined, and only from their point of their definition onward.

Note that:

  • private on a member means “private to the enclosing type or module.”
  • private on a function or value definition in a module means “private to the module or namespace declaration group.”
  • private on a type, module, or type representation in a module means “private to the module.”

Non-public entities are not accessible from outside their defining scope.

Clef Note: Accessibility is enforced at compile time. Non-public symbols are not exported in the native binary’s symbol table.

Note: The family and protected specifications are not supported in this version of the F# language.

Accessibility modifiers can appear only in the locations summarized in the following table.

ComponentLocationExample
Function or value
definition in module
Precedes identifierlet private x = 1
let inline private f x = 1
let mutable private x = 1
Module definitionPrecedes identifiermodule private M =
let x = 1
Type definitionPrecedes identifiertype private C = A | B
type private C<'T> = A | B
val definition in a classPrecedes identifierval private x : int
Explicit constructorPrecedes identifierprivate new () = { inherit Base }
Implicit constructorPrecedes identifiertype C private() = ...
Member definitionPrecedes identifier, but cannot appear on
inherit definitions,
interface definitions,
abstract definitions,
individual union cases.
Accessibility for
inherit, interface,
and abstract definitions is
always the same as that of
the enclosing class.
member private x.X = 1
Explicit property get
or set in a class
Precedes identifiermember __.Item
with private get i = 1
and private set i v = ()
Type representationPrecedes identifiertype Cases = private | A | B