Namespace and Module Signatures
A module signature specifies the functionality exposed by a module: the public values, types, and nested modules. Signatures hide implementation details.
Clef has no separate signature files. CCS does not recognize a distinct interface artifact,
and no file extension is reserved for one. A module’s public surface is described in the same
source file as its implementation, using an inline signature ... end block placed immediately
before the module declaration it constrains.
CCS computes compilation order from whole-program syntactic dependency analysis (§ Compilation Pipeline). It checks clients against the corresponding module implementations and their inline signatures. A signature may expose a type abstractly while concealing its representation (§ Abstract Type Signatures).
signature-decl :=
signature long-ident~opt = signature-body -- inline module signature, placed
-- immediately before the module it constrains
signature-body :=
begin module-signature-element ... module-signature-element end
module-signature-element :=
val mutable~opt curried-sig -- value signature
val value-defn -- literal value signature
type type-signatures -- type signature(s) revealing representation
type type-name -- abstract (opaque) type signature; see below
exception exception-signature -- exception signature
signature-decl -- nested submodule signature
module-abbrev -- local alias for a module
import-decl -- locally import contents of a module
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 endThe begin and end tokens are optional when lightweight syntax is used.
Abstract Type Signatures
A type signature that gives a representation — type T = { ... }, type T = union-cases, or
type T = ty — reveals that representation to clients. A type signature that gives only the type
name, with no = and no representation, exposes the type abstractly:
signature
type Handle -- abstract: name visible, representation hidden
val create : unit -> Handle
val close : Handle -> unit
end
module Resource =
type Handle = { fd: int; generation: int }
let create () = ...
let close h = ...Clients of Resource may name Handle, hold values of it, and pass them to create and close,
but cannot observe or depend on its representation (fd, generation). This is the single most
important capability the signature mechanism provides, and it is fully expressible inline: opacity is
a property of what a type signature reveals, not of whether the signature lives in a separate file.
The all-or-none rules in § Signature Conformance govern partial revelation: a record or union type is either fully abstract (name only) or fully revealed (all fields or cases, in declaration order); it cannot expose a subset.
Signature declarations, like module declarations, are processed in dependency order computed by CCS (see § Compilation Pipeline). The following inline signature is well-formed regardless of the textual order of the two namespaces:
namespace Utilities.Part1
module Module1 =
val x : Utilities.Part2.StorageCache
namespace Utilities.Part2
type StorageCache =
new : unit -> unitSignature 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.
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)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)
endA 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 -> CatThe 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 = kindSignature 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 = tyin an implementation does not match typeT(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
inheritdeclaration, abstract dispatch slot declarations, or abstract interface declarations.
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, andmutablemodifiers 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> -> 'Tand the compiler could infer the typeval head : seq<'a> -> 'afrom the implementation. These are considered identical up to renaming the generic parameters. - The arities must match, as described in the next section.
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 - > rtyArities 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 -> intand
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 + 1Arity inference also permits a right-hand-side function expression:
let F = fun x -> x + 1The second signature
val F: (int -> int)can be satisfied by any value of the appropriate type. For example:
let f =
let mutable cache = Map.empty<int,int>
fun x ->
match Map.tryFind x cache with
| Some v -> v
| None ->
let res = x * x
cache <- Map.add x res cache
resor
let f = fun x -> x + 1or
// 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 function arity affects compilation. F# functions with known arity compile to direct function calls, while function values require closure allocation. Signatures must contain enough information to reveal the desired arity for efficient native code generation.
Clef Note: In native compilation, arity information enables direct function calls without closure overhead. Parenthesized signatures indicate that the implementation must be callable with that arity, enabling the compiler to generate efficient native calling conventions.
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 (§ Generalization).
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, andoverridequalifiers must match precisely.Abstract members must be present in the signature if a representation is given for a type.