Skip to content

11. Namespace and Module Signatures

A signature file contains one or more namespace or module signatures, and specifies the functionality that is implemented by its corresponding implementation file. It also can hide functionality that the corresponding implementation file contains.

namespace-decl-group-signature :=
    namespace long-ident module-signature-elements

module-signature :=
    module ident = module-signature-body

module-signature-element :=
    val mutable~opt curried-sig     -- value signature
    val value-defn                  -- literal value signature
    type type-signatures            -- type(s) signature
    exception exception-signature   -- exception signature
    module-signature                -- submodule signature
    module-abbrev                   -- local alias for a module
    import-decl                     -- locally import contents of a module

module-signature-elements := module-signature-element ... module-signature-element

module-signature-body :=
    begin module-signature-elements end

type-signature :=
    abbrev-type-signature
    record-type-signature
    union-type-signature
    anon-type-signature
    class-type-signature
    struct-type-signature
    interface-type-signature
    enum-type-signature
    delegate-type-signature
    type-extension-signature

type-signatures := type-signature ... and ... type-signature

type-signature-element :=
    attributesopt access~opt new : uncurried-sig        -- constructor signature
    attributesopt member acces~opt member-sig           -- member signature
    attributesopt abstract access~opt member-sig        -- member signature
    attributesopt override member-sig                   -- member signature
    attributesopt default member-sig                    -- member signature
    attributes~opt static member access~opt member-sig  -- static member signature
    interface type                                      -- interface signature

abbrev-type-signature := type-name '=' type

union-type-signature := type-name '=' union-type-cases type-extension-elements-
    signature~opt

record-type-signature := type-name '=' '{' record-fields '}' type-extension-
    elements-signature~opt

anon-type-signature := type-name '=' begin type-elements-signature end

class-type-signature := type-name '=' class type-elements-signature end

struct-type-signature := type-name '=' struct type-elements-signature end

interface-type-signature := type-name '=' interface type-elements-signature end

enum-type-signature := type-name ' =' enum-type-cases

delegate-type-signature := type-name ' =' delegate-sig

type-extension-signature := type-name type-extension-elements-signature

type-extension-elements-signature := with type-elements-signature end

The begin and end tokens are optional when lightweight syntax is used.

Like module declarations, signature declarations are processed sequentially rather than simultaneously, so that later signature declarations are not in scope when earlier ones are processed.

namespace Utilities.Part1

    module Module1 =
        val x : Utilities.Part2.StorageCache // error (Part2 not yet declared)

namespace Utilities.Part2

    type StorageCache =
        new : unit -> unit

11.1 Signature Elements

A namespace or module signature declares one or more value signatures and one or more type definition signatures. A type definition signature may include one or more member signatures , in addition to other elements of type definitions that are specified in the signature grammar at the start of this chapter.

11.1.1 Value Signatures

A value signature indicates that a value exists in the implementation. For example, in the signature of a module, the following declares two value signatures:

module MyMap =
    val mapForward : index1: int * index2: int -> string
    val mapBackward : name: string -> (int * int)

The corresponding implementation file might contain the following implementation:

module MyMap =
    let mapForward (index1:int, index2:int) = string index1 + "," + string index2
    let mapBackward (name:string) = (0, 0)

11.1.2 Type Definition and Member Signatures

A type definition signature indicates that a corresponding type definition appears in the implementation. For example, in an interface type, the following declares a type definition signature for Forward and Backward:

type IMap =
    interface
        abstract Forward : index1: int * index2: int -> string
        abstract Backward : name: string -> (int * int)
    end

A member signature indicates that a corresponding member appears on the corresponding type definition in the implementation. Member specifications must specify argument and return types, and can optionally specify names and attributes for parameters.

For example, the following declares a type definition signature for a type with one constructor member, one property member Kind and one method member Purr:

type Cat =
    new : kind:string -> Cat
    member Kind : string
    member Purr : unit -> Cat

The corresponding implementation file might contain the following implementation:

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

11.2 Signature Conformance

Values, types, and members that are present in implementations can be omitted in signatures, with the following exceptions:

  • Type abbreviations may not be hidden by signatures. That is, a type abbreviation type T = ty in an implementation does not match type T (without an abbreviation) in a signature.
  • Any type that is represented as a record or union must reveal either all or none of its fields or cases, in the same order as that specified in the implementation. Types that are represented as classes may reveal some, all, or none of their fields in a signature.
  • Any type that is revealed to be an interface, or a type that is a class or struct with one or more constructors may not hide its inherit declaration, abstract dispatch slot declarations, or abstract interface declarations.

Note: This section does not yet document all checks made by the F# 3.1 language implementation.

11.2.1 Signature Conformance for Functions and Values

If both a signature and an implementation contain a function or value definition with a given name, the signature and implementation must conform as follows:

  • The declared accessibilities, inline, and mutable modifiers must be identical in both the signature and the implementation.
  • If either the signature or the implementation has the [<Literal>] attribute, both must have this attribute. Furthermore, the declared literal values must be identical.
  • The number of generic parameters—both inferred and explicit—must be identical.
  • The types and type constraints must be identical up to renaming of inferred and/or explicit generic parameters. For example, assume a signature is written val head : seq<'T> -> 'T and the compiler could infer the type val head : seq<'a> -> 'a from the implementation. These are considered identical up to renaming the generic parameters.
  • The arities must match, as described in the next section.

11.2.1.1 Arity Conformance for Functions and Values

Arities of functions and values must conform between implementation and signature. Arities of values are implicit in module signatures. A signature that contains the following results in the arity [A1 ... An] for F:

val F : ty11 * ... * ty1A1 - > ... -> tyn1 * ... * tynAn - > rty

Arities in a signature must be equal to or shorter than the corresponding arities in an implementation, and the prefix must match. This means that F# makes a deliberate distinction between the following two signatures:

val F: int -> int

and

val F: (int -> int)

The parentheses indicate a top-level function, which might be a first-class computed expression that computes to a function value, rather than a compile-time function value.

The first signature can be satisfied only by a true function; that is, the implementation must be a lambda value as in the following:

let F x = x + 1

Note: Because arity inference also permits right-hand-side function expressions, the implementation may currently also be:
let F = fun x -> x + 1

The second signature

val F: (int -> int)

can be satisfied by any value of the appropriate type. For example:

let f =
    let myTable = new System.Collections.Generic.Dictionary<int,int>(4)
    fun x ->
        if myTable.ContainsKey x then
            myTable.[x]
        else
            let res = x * x
            myTable.[x] <- res
            res

—or—

let f = fun x -> x + 1

—or—

// throw an exception as soon as the module initialization is triggered
let f : int -> int = failwith "failure"

For both the first and second signatures, you can still use the functions as first-class function values from client code—the parentheses simply act as a constraint on the implementation of the value.

The reason for this interpretation of types in value and member signatures is that CLI interoperability requires that F# functions compile to methods, rather than to fields that are function values. Thus, signatures must contain enough information to reveal the desired arity of a method as it is revealed to other CLI programming languages.

11.2.1.2 Signature Conformance for Type Functions

If a value is a type function, then its corresponding value signature must have explicit type arguments. For example, the implementation

let empty<'T> : list<'T> = printfn "hello"; []

conforms to this signature:

val empty<'T> : list<'T>

but not to this signature:

val empty : list<'T>

The reason for this rule is that the second signature indicates that the value is, by default, generalizable (§14.6.7).

11.2.2 Signature Conformance for Members

If both a signature and an implementation contain a member with a given name, the signature and implementation must conform as follows:

  • If one is an extension member, both must be extension members.
  • If one is a constructor, then both must be constructors.
  • If one is a property, then both must be properties.

  • The types must be identical up to renaming of inferred or explicit type parameters (as for functions and values).

  • The static, abstract, and override qualifiers must match precisely.
  • Abstract members must be present in the signature if a representation is given for a type.

Note: This section does not yet document all checks made by the F# 3 .1 language implementation.