5. Types and Type Constraints
The notion of type is central to both the static checking of F# programs and to dynamic type tests and reflection at runtime. The word is used with four distinct but related meanings:
- Type definitions, such as the actual CLI or F# definitions of
System.String
orFSharp.Collections.Map<_,_>
. - Syntactic types, such as the text
option<_>
that might occur in a program text. Syntactic types are converted to static types during the process of type checking and inference. - Static types, which result from type checking and inference, either by the translation of syntactic
types that appear in the source text, or by the application of constraints that are related to
particular language constructs. For example,
option<int>
is the fully processed static type that is inferred for an expressionSome(1+1)
. Static types may containtype variables
as described later in this section. - Runtime types, which are objects of type
System.Type
and represent some or all of the information that type definitions and static types convey at runtime. Theobj.GetType()
method, which is available on all F# values, provides access to the runtime type of an object. An object’s runtime type is related to the static type of the identifiers and expressions that correspond to the object. Runtime types may be tested by built-in language operators such as:?
and:?>
, the expression form downcastexpr
, and pattern matching type tests. Runtime types of objects do not contain type variables. Runtime types thatSystem.Reflection
reports may contain type variables that are represented bySystem.Type
values.
The following describes the syntactic forms of types as they appear in programs:
type :=
( type )
type - > type -- function type
type * ... * type -- tuple type
struct (type * ... * type) -- struct tuple type
typar -- variable type
long-ident -- named type, such as int
long-ident <type-args> -- named type, such as list<int>
long-ident < > -- named type, such as IEnumerable< >
type long-ident -- named type, such as int list
type [ , ... , ] -- array type
type typar-defns -- type with constraints
typar :> type -- variable type with subtype constraint
# type -- anonymous type with subtype constraint
type-args := type-arg , ..., type-arg
type-arg :=
type -- type argument
measure -- unit of measure argument
static-parameter -- static parameter
atomic-type :=
type : one of
#type typar ( type ) long-ident long-ident <type-args>
typar :=
_ -- anonymous variable type
' ident -- type variable
^ ident -- static head-type type variable
constraint :=
typar :> type -- coercion constraint
typar : null -- nullness constraint
static-typars : ( member-sig ) -- member "trait" constraint
typar : (new : unit -> 'T) -- CLI default constructor constraint
typar : struct -- CLI non-Nullable struct
typar : not struct -- CLI reference type
typar : enum< type > -- enum decomposition constraint
typar : unmanaged -- unmanaged constraint
typar : delegate<type, type> -- delegate decomposition constraint
typar : equality
typar : comparison
typar-defn := attributes opt typar
typar-defns := < typar-defn, ..., typar-defn typar-constraints opt >
typar-constraints := when constraint and ... and constraint
static-typars :=
^ ident
(^ ident or ... or ^ ident )
member-sig := <see Section 10>
In a type instantiation, the type name and the opening angle bracket must be syntactically adjacent with no intervening whitespace, as determined by lexical filtering (§15.). Specifically:
array<int>
and not
array < int >
5.1 Checking Syntactic Types
Syntactic types are checked and converted to static types as they are encountered. Static types are a specification device used to describe
- The process of type checking and inference.
- The connection between syntactic types and the execution of F# programs.
Every expression in an F# program is given a unique inferred static type, possibly involving one or more explicit or implicit generic parameters.
For the remainder of this specification we use the same syntax to represent syntactic types and
static types. For example int32 * int32
is used to represent the syntactic type that appears in
source code and the static type that is used during checking and type inference.
The conversion from syntactic types to static types happens in the context of a name resolution environment (see §14.1), a floating type variable environment, which is a mapping from names to type variables, and a type inference environment (see §14.5).
The phrase “fresh type” means a static type that is formed from a fresh type inference variable. Type
inference variables are either solved or generalized by type inference (§14.5). During conversion and
throughout the checking of types, expressions, declarations, and entire files, a set of current
inference constraints is maintained. That is, each static type is processed under input constraints Χ
,
and results in output constraints Χ’
. Type inference variables and constraints are progressively
simplified and eliminated based on these equations through constraint solving (§14.5).
5.1.1 Named Types
Named types have several forms, as listed in the following table.
Form | Description |
---|---|
long-ident <ty1, ..., tyn> |
Named type with one or more suffixed type arguments. |
long-ident |
Named type with no type arguments |
type long-ident |
Named type with one type argument; processed the same as long-ident<type> |
ty1 -> ty2 |
A function type, where: ▪ ty1 is the domain of the function values associated with the type ▪ ty2 is the range. In compiled code it is represented by the named type FSharp.Core.FastFunc<ty1, ty2> . |
Named types are converted to static types as follows:
- Name Resolution for Types (see §14.1) resolves
long-ident
to a type definition with formal generic parameters<typar1, ..., typarn>
and formal constraintsC
. The number of type argumentsn
is used during the name resolution process to distinguish between similarly named types that take different numbers of type arguments. - Fresh type inference variables
<ty'1, ..., ty'n>
are generated for each formal type parameter. The formal constraintsC
are added to the current inference constraints for the new type inference variables; and constraintstyi = ty'i
are added to the current inference constraints.
5.1.2 Variable Types
A type of the form 'ident
is a variable type. For example, the following are all variable types:
'a
'T
'Key
During checking, Name Resolution (see §14.1) is applied to the identifier.
- If name resolution succeeds, the result is a variable type that refers to an existing declared type parameter.
- If name resolution fails, the current floating type variable environment is consulted, although only in the context of a syntactic type that is embedded in an expression or pattern. If the type variable name is assigned a type in that environment, F# uses that mapping. Otherwise, a fresh
type inference variable is created (see §14.5) and added to both the type inference environment and the floating type variable environment.
A type of the form _
is an anonymous variable type. A fresh type inference variable is created and
added to the type inference environment (see §14.5) for such a type.
A type of the form ^ident
is a statically resolved type variable. A fresh type inference variable is
created and added to the type inference environment (see §14.5). This type variable is tagged with
an attribute that indicates that it can be generalized only at inline
definitions (see §14.6.7). The
same restriction on generalization applies to any type variables that are contained in any type that is
equated with the ^ident
type in a type inference equation.
Note: This specification generally uses uppercase identifiers such as
'T
or'Key
for user-declared generic type parameters, and uses lowercase identifiers such as'a
or'b
for compiler-inferred generic parameters.
5.1.3 Tuple Types
A tuple type has the following form:
ty 1 * ... * tyn
The elaborated form of a tuple type is shorthand for a use of the family of F# library types
System.Tuple<_, ..., _>
. (see §6.3.2) for the details of this encoding.
When considered as static types, tuple types are distinct from their encoded form. However, the
encoded form of tuple types is visible in the F# type system through runtime types. For example,
typeof<int * int>
is equivalent to typeof<System.Tuple<int,int>>
.
5.1.3.1 Struct Tuple Types
A struct tuple type has the following form:
struct ( ty 1 * ... * tyn )
The elaborated form of a tuple type is shorthand for a use of the family of .NET types System.ValueTuple.
When considered as static types, tuple types are distinct from their encoded form. However, the
encoded form of tuple types is visible in the F# type system through runtime types. For example,
typeof<int * int>
is equivalent to typeof<System.ValueTuple<int,int>>
.
Struct tuple types are value types (as opposed to tuple types which are reference types). Struct tuple types are primarily aimed at use in interop and performance tuning.
The "structness" (i.e. tuple type vs. struct tuple type) of tuple expressions and tuple patterns is inferred in the F# type inference process (unless they are explicitly tagged "struct"). However, code cannot be generic over structness, and there is no implicit conversion between struct tuples and reference tuples.
5.1.4 Array Types
Array types have the following forms:
ty []
ty [ , ... , ]
A type of the form ty []
is a single-dimensional array type, and a type of the form ty[ , ... , ]
is a
multidimensional array type. For example, int[,,]
is an array of integers of rank 3.
Except where specified otherwise in this document, these array types are treated as named types, as
if they are an instantiation of a fictitious type definition System.Arrayn<ty>
where n
corresponds to
the rank of the array type.
Note: The type
int[][,]
in F# is the same as the typeint[,][]
in C# although the dimensions are swapped. This ensures consistency with other postfix type names in F# such asint list list
.
F# supports multidimensional array types only up to rank 4.
5.1.5 Constrained Types
A type with constraints has the following form:
type when constraints
During checking, type
is first checked and converted to a static type, then constraints
are checked
and added to the current inference constraints. The various forms of constraints are described
in (see §5.2)
A type of the form typar :> type
is a type variable with a subtype constraint and is equivalent to
typar when typar :> type
.
A type of the form #type
is an anonymous type with a subtype constraint and is equivalent to 'a
when 'a :> type
, where 'a
is a fresh type inference variable.
5.2 Type Constraints
A type constraint limits the types that can be used to create an instance of a type parameter or type variable. F# supports the following type constraints:
- Subtype constraints
- Nullness constraints
- Member constraints
- Default constructor constraints
- Value type constraints
- Reference type constraints
- Enumeration constraints
- Delegate constraints
- Unmanaged constraints
- Equality and comparison constraints
5.2.1 Subtype Constraints
An explicit subtype constraint has the following form:
typar :> type
During checking, typar
is first checked as a variable type, type
is checked as a type, and the
constraint is added to the current inference constraints. Subtype constraints affect type coercion as
specified in (see §5.4.7)
Note that subtype constraints also result implicitly from:
- Expressions of the form
expr :> type
. - Patterns of the form
pattern :> type
. - The use of generic values, types, and members with constraints.
- The implicit use of subsumption when using values and members (see §14.4.3).
A type variable cannot be constrained by two distinct instantiations of the same named type. If two
such constraints arise during constraint solving, the type instantiations are constrained to be equal.
For example, during type inference, if a type variable is constrained by both IA<int>
and IA<string>
,
an error occurs when the type instantiations are constrained to be equal. This limitation is
specifically necessary to simplify type inference, reduce the size of types shown to users, and help
ensure the reporting of useful error messages.
5.2.2 Nullness Constraints
An explicit nullness constraint has the following form:
typar : null
During checking, typar
is checked as a variable type and the constraint is added to the current
inference constraints. The conditions that govern when a type satisfies a nullness constraint are
specified in (see §5.4.8)
In addition:
- The
typar
must be a statically resolved type variable of the form^ident
. This limitation ensures that the constraint is resolved at compile time, and means that generic code may not use this constraint unless that code is marked inline (see §14.6.7).
Note: Nullness constraints are primarily for use during type checking and are used relatively rarely in F# code.
Nullness constraints also arise from expressions of the formnull
.
5.2.3 Member Constraints
An explicit member constraint has the following form:
( typar or ... or typar ) : ( member-sig )
For example, the F# library defines the + operator with the following signature:
val inline (+) : ^a -> ^b -> ^c
when (^a or ^b) : (static member (+) : ^a * ^b -> ^c)
This definition indicates that each use of the +
operator results in a constraint on the types that
correspond to parameters ^a
, ^b
, and ^c
. If these are named types, then either the named type for
^a
or the named type for ^b
must support a static member called +
that has the given signature.
In addition:
- Each
typar
must be a statically resolved type variable (see §5.1.2) in the form^ident
. This ensures that the constraint is resolved at compile time against a corresponding named type. It also means that generic code cannot use this constraint unless that code is marked inline (see §14.6.7). - The
member-sig
cannot be generic; that is, it cannot include explicit type parameter definitions. - The conditions that govern when a type satisfies a member constraint are specified in (see §14.5.4).
Note: Member constraints are primarily used to define overloaded functions in the F# library and are used relatively rarely in F# code.
Uses of overloaded operators do not result in generalized code unless definitions are marked as inline. For example, the function
let f x = x + x
results in a functionf
that can be used only to add one type of value, such asint
orfloat
. The exact type is determined by later constraints.
A type variable may not be involved in the support set of more than one member constraint that has the same name, staticness, argument arity, and support set (see §14.5.4). If it is, the argument and return types in the two member constraints are themselves constrained to be equal. This limitation is specifically necessary to simplify type inference, reduce the size of types shown to users, and ensure the reporting of useful error messages.
5.2.4 Default Constructor Constraints
An explicit default constructor constraint has the following form:
typar : (new : unit -> 'T)
During constraint solving (see §14.5), the constraint type : (new : unit -> 'T)
is met if type
has a
parameterless object constructor.
Note: This constraint form exists primarily to provide the full set of constraints that CLI implementations allow. It is rarely used in F# programming.
5.2.5 Value Type Constraints
An explicit value type constraint has the following form:
typar : struct
During constraint solving (see §14.5), the constraint type
: struct is met if type
is a value type other
than the CLI type System.Nullable<_>
.
Note: This constraint form exists primarily to provide the full set of constraints that CLI implementations allow. It is rarely used in F# programming.
The restriction onSystem.Nullable
is inherited from C# and other CLI languages, which give this type a special syntactic status. In F#, the typeoption<_>
is similar to some uses ofSystem.Nullable<_>
. For various technical reasons the two types cannot be equated, notably because types such asSystem.Nullable<System.Nullable<_>>
andSystem.Nullable<string>
are not valid CLI types.
5.2.6 Reference Type Constraints
An explicit reference type constraint has the following form:
typar : not struct
During constraint solving (see §14.5), the constraint type : not struct
is met if type
is a reference type.
Note: This constraint form exists primarily to provide the full set of constraints that CLI implementations allow. It is rarely used in F# programming.
5.2.7 Enumeration Constraints
An explicit enumeration constraint has the following form:
typar : enum<underlying-type>
During constraint solving (see §14.5), the constraint type : enum<underlying-type>
is met if type
is a CLI
or F# enumeration type that has constant literal values of type underlying-type
.
Note: This constraint form exists primarily to allow the definition of library functions such as
enum
. It is rarely used directly in F# programming.
Theenum
constraint does not imply anything about subtypes. For example, anenum
constraint does not imply that the type is a subtype ofSystem.Enum
.
5.2.8 Delegate Constraints
An explicit delegate constraint has the following form:
typar : delegate< tupled-arg-type , return-type>
During constraint solving (see §14. .5), the constraint type : delegate<tupled-arg-type, return-types>
is met if type
is a delegate type D
with declaration type D = delegate of object * arg1 * ... *
argN
and tupled-arg-type = arg1 * ... * argN.
That is, the delegate must match the CLI design
pattern where the sender object is the first argument to the event.
Note: This constraint form exists primarily to allow the definition of certain F# library functions that are related to event programming. It is rarely used directly in F# programming.
Thedelegate
constraint does not imply anything about subtypes. In particular, adelegate
constraint does not imply that the type is a subtype ofSystem.Delegate
.
Thedelegate
constraint applies only to delegate types that follow the usual form for CLI event handlers, where the first argument is asender
object. The reason is that the purpose of the constraint is to simplify the presentation of CLI event handlers to the F# programmer.
5.2.9 Unmanaged Constraints
An unmanaged constraint has the following form:
typar : unmanaged
During constraint solving (see §14.5), the constraint type : unmanaged
is met if type
is unmanaged as
specified below:
- Types sbyte,
byte
,char
,nativeint
,unativeint
,float32
,float
,int16
,uint16
,int32
,uint32
,int64
,uint64
,decimal
are unmanaged. - Type
nativeptr<type>
is unmanaged. - A non-generic struct type whose fields are all unmanaged types is unmanaged.
5.2.10 Equality and Comparison Constraints
Equality constraints and comparison constraints have the following forms, respectively:
typar : equality
typar : comparison
During constraint solving (see §14.5), the constraint type : equality
is met if both of the following
conditions are true:
- The type is a named type, and the type definition does not have, and is not inferred to have, the
NoEquality
attribute. - The type has
equality
dependenciesty1,..., tyn
, each of which satisfiestyi: equality
.
The constraint type : comparison
is a comparison constraint
. Such a constraint is met if all the
following conditions hold:
- If the type is a named type, then the type definition does not have, and is not inferred to have,
the
NoComparison
attribute, and the type definition implementsSystem.IComparable
or is an array type or isSystem.IntPtr
or isSystem.UIntPtr
. - If the type has
comparison dependencies
ty1, ..., tyn
, then each of these must satisfytyi : comparison
An equality constraint is a relatively weak constraint, because with two exceptions, all CLI types
satisfy this constraint. The exceptions are F# types that are annotated with the NoEquality
attribute
and structural types that are inferred to have the NoEquality
attribute. The reason is that in other
CLI languages, such as C#, it possible to use reference equality on all reference types.
A comparison constraint is a stronger constraint, because it usually implies that a type must
implement System.IComparable
.
5.3 Type Parameter Definitions
Type parameter definitions can occur in the following locations:
- Value definitions in modules
- Member definitions
- Type definitions
- Corresponding specifications in signatures
For example, the following defines the type parameter ‘T in a function definition:
let id<'T> (x:'T) = x
Likewise, in a type definition:
type Funcs<'T1,'T2> =
{ Forward: 'T1 -> 'T2
Backward : 'T2 -> 'T2 }
Likewise, in a signature file:
val id<'T> : 'T -> 'T
Explicit type parameter definitions can include explicit constraint declarations
. For example:
let dispose2<'T when 'T :> System.IDisposable> (x: 'T, y: 'T) =
x.Dispose()
y.Dispose()
The constraint in this example requires that 'T
be a type that supports the IDisposable
interface.
However, in most circumstances, declarations that imply subtype constraints on arguments can be written more concisely:
let throw (x: Exception) = raise x
Multiple explicit constraint declarations use and:
let multipleConstraints<'T when 'T :> System.IDisposable and
'T :> System.IComparable > (x: 'T, y: 'T) =
if x.CompareTo(y) < 0 then x.Dispose() else y.Dispose()
Explicit type parameter definitions can declare custom attributes on type parameter definitions (see §13.1).
5.4 Logical Properties of Types
During type checking and elaboration, syntactic types and constraints are processed into a reduced form composed of:
- Named types
op<types>
, where eachop
consists of a specific type definition, an operator to form function types, an operator to form array types of a specific rank, or an operator to form specificn-tuple
types. - Type variables
'ident
.
5.4.1 Characteristics of Type Definitions
Type definitions include CLI type definitions such as System.String
and types that are defined in F#
code (see §8.). The following terms are used to describe type definitions:
- Type definitions may be generic , with one or more type parameters; for example,
System.Collections.Generic.Dictionary<'Key,'Value>
. - The generic parameters of type definitions may have associated
formal type constraints
. - Type definitions may have custom attributes (see §13.1), some of which are relevant to checking and inference.
-
Type definitions may be type abbreviations (see §8.3). These are eliminated for the purposes of checking and inference (see §5.4.2).
-
Type definitions have a
kind
which is one of the following: Class
Interface
Delegate
Struct
Record
Union
Enum
Measure
Abstract
The kind is determined at the point of declaration by Type Kind Inference (see §8.2) if it is not
specified explicitly as part of the type definition. The kind of a type refers to the kind of its
outermost named type definition, after expanding abbreviations. For example, a type is a class
type if it is a named type C<types>
where C
is of kind class. Thus,
System.Collections.Generic.List<int>
is a class type.
- Type definitions may be sealed. Record, union, function, tuple, struct, delegate, enum, and array
types are all sealed, as are class types that are marked with the
SealedAttribute
attribute. - Type definitions may have zero or one base type declarations. Each base type declaration represents an additional type that is supported by any values that are formed using the type definition. Furthermore, some aspects of the base type are used to form the implementation of the type definition.
- Type definitions may have one or more interface declarations. These represent additional encapsulated types that are supported by values that are formed using the type.
Class, interface, delegate, function, tuple, record, and union types are all reference type definitions. A type is a reference type if its outermost named type definition is a reference type, after expanding type definitions.
Struct types are value types.
5.4.2 Expanding Abbreviations and Inference Equations
Two static types are considered equivalent and indistinguishable if they are equivalent after taking into account both of the following:
- The inference equations that are inferred from the current inference constraints (see §14.5).
- The expansion of type abbreviations (see §8.3).
For example, static types may refer to type abbreviations such as int
, which is an abbreviation for
System.Int32
and is declared by the F# library:
type int = System.Int32
This means that the types int32
and System.Int32
are considered equivalent, as are System.Int32 -> int
and int -> System.Int32
.
Likewise, consider the process of checking this function:
let checkString (x:string) y =
(x = y), y.Contains("Hello")
During checking, fresh type inference variables are created for values x
and y
; let’s call them ty1
and
ty2
. Checking imposes the constraints ty1 = string
and ty1 = ty2
. The second constraint results
from the use of the generic =
operator. As a result of constraint solving, ty2 = string
is inferred, and
thus the type of y
is string
.
All relations on static types are considered after the elimination of all equational inference
constraints and type abbreviations. For example, we say int
is a struct type because System.Int32
is
a struct type.
Note: Implementations of F# should attempt to preserve type abbreviations when reporting types and errors to users. This typically means that type abbreviations should be preserved in the logical structure of types throughout the checking process.
5.4.3 Type Variables and Definition Sites
Static types may be type variables. During type inference, static types may be partial, in that they contain type inference variables that have not been solved or generalized. Type variables may also refer to explicit type parameter definitions, in which case the type variable is said to be rigid and have a definition site.
For example, in the following, the definition site of the type parameter 'T is the type definition of C:
type C<'T> = 'T * 'T
Type variables that do not have a binding site are inference variables. If an expression is composed of multiple sub-expressions, the resulting constraint set is normally the union of the constraints that result from checking all the sub-expressions. However, for some constructs (notably function, value and member definitions), the checking process applies generalization (see §14.6.7). Consequently, some intermediate inference variables and constraints are factored out of the intermediate constraint sets and new implicit definition site(s) are assigned for these variables.
For example, given the following declaration, the type inference variable that is associated with the
value x
is generalized and has an implicit definition site at the definition of function id
:
let id x = x
Occasionally in this specification we use a more fully annotated representation of inferred and generalized type information. For example:
let id<'a> x'a = x'a
Here, 'a
represents a generic type parameter that is inferred by applying type inference and
generalization to the original source code (see §14.6.7), and the annotation represents the definition site
of the type variable.
5.4.4 Base Type of a Type
The base type for the static types is shown in the table. These types are defined in the CLI specifications and corresponding implementation documentation.
Static Type | Base Type |
---|---|
Abstract types | System.Object |
All array types | System.Array |
Class types | The declared base type of the type definition if the type has one; otherwise,System.Object . For generic types C<type-inst> , substitute the formal generic parameters of C for type-inst |
Delegate types | System.MulticastDelegate |
Enum types | System.Enum |
Exception types | System.Exception |
Interface types | System.Object |
Record types | System.Object |
Struct types | System.ValueType |
Union types | System.Object |
Variable types | System.Object |
5.4.5 Interfaces Types of a Type
The interface types of a named type C<type-inst>
are defined by the transitive closure of the
interface declarations of C
and the interface types of the base type of C
, where formal generic
parameters are substituted for the actual type instantiation type-inst
.
The interface types for single dimensional array types ty[]
include the transitive closure that starts
from the interface System.Collections.Generic.IList<ty>
, which includes
System.Collections.Generic.ICollection<ty>
and System.Collections.Generic.IEnumerable<ty>
.
5.4.6 Type Equivalence
Two static types ty1
and ty2
are definitely equivalent (with respect to a set of current inference
constraints) if either of the following is true:
ty1
has formop<ty11, ..., ty1n>
,ty2
has formop<ty21, ..., ty2n>
and eachty1i
is definitely equivalent toty2i
for all1
<=i
<=n
.
OR
ty1
andty2
are both variable types, and they both refer to the same definition site or are the same type inference variable.
This means that the addition of new constraints may make types definitely equivalent where
previously they were not. For example, given Χ = { 'a = int }
, we have list<int>
= list<'a>
.
Two static types ty1
and ty2
are feasibly equivalent if ty1
and ty2
may become definitely equivalent if
further constraints are added to the current inference constraints. Thus list<int>
and list<'a>
are
feasibly equivalent for the empty constraint set.
5.4.7 Subtyping and Coercion
A static type ty2
coerces to static type ty1
(with respect to a set of current inference constraints X), if
ty1
is in the transitive closure of the base types and interface types of ty2
. Static coercion is written
with the :>
symbol:
ty2 :> ty1
Variable types 'T
coerce to all types ty
if the current inference constraints include a constraint of the
form 'T :> ty2
, and ty
is in the inclusive transitive closure of the base and interface types of ty2
.
A static type ty2
feasibly coerces to static type ty1
if ty2
coerces to ty1
may hold through the addition
of further constraints to the current inference constraints. The result of adding constraints is defined
in Constraint Solving
(see §14.5).
5.4.8 Nullness
The design of F# aims to greatly reduce the use of null literals in common programming tasks, because they generally result in error-prone code. However:
- The use of some
null
literals is required for interoperation with CLI libraries. - The appearance of
null
values during execution cannot be completely precluded for technical reasons related to the CLI and CLI libraries.
As a result, F# types differ in their treatment of the null
literal and null
values. All named types and
type definitions fall into one of the following categories:
- Types with the
null
literal. These types havenull
as an "extra" value. The following types are in this category: - All CLI reference types that are defined in other CLI languages.
- All types that are defined in F# and annotated with the
AllowNullLiteral
attribute.
For example, System.String
and other CLI reference types satisfy this constraint, and these types
permit the direct use of the null
literal.
- Types with
null
as an abnormal value. These types do not permit thenull
literal, but do havenull
as an abnormal value. The following types are in this category: - All F# list, record, tuple, function, class, and interface types.
- All F# union types except those that have
null
as a normal value, as discussed in the next bullet point.
For types in this category, the use of the null
literal is not directly allowed. However, strictly
speaking, it is possible to generate a null
value for these types by using certain functions such as
Unchecked.defaultof<type>
. For these types, null
is considered an abnormal value. Operations
differ in their use and treatment of null
values; for details about evaluation of expressions that
might include null
values, see (see §6.9).
- Types with
null
as a representation value. These types do not permit thenull
literal but use thenull
value as a representation. For these types, the use of the null literal is not directly permitted. However, one or all of the “normal” values of the type is represented by the null value. The following types are in this category: - The unit type. The
null
value is used to represent all values of this type. - Any union type that has the
FSharp.Core.CompilationRepresentation(CompilationRepresentationFlags.UseNullAsTrueV alue)
attribute flag and a single null union case. The null value represents this case. In particular,null
representsNone
in the F#option<_>
type. - Types without
null
. These types do not permit thenull
literal and do not have the null value. All value types are in this category, including primitive integers, floating-point numbers, and any value of a CLI or F#struct
type.
A static type ty
satisfies a nullness constraint ty : null
if it:
- Has an outermost named type that has the
null
literal. - Is a variable type with a
typar : null
constraint.
5.4.9 Default Initialization
Related to nullness is the default initialization of values of some types to zero values. This technique is common in some programming languages, but the design of F# deliberately de-emphasizes it. However, default initialization is allowed in some circumstances:
- Checked default initialization may be used when a type is known to have a valid and “safe”
default zero value. For example, the types of fields that are labeled with
DefaultValue(true)
are checked to ensure that they allow default initialization. - CLI libraries sometimes perform unchecked default initialization, as do the F# library primitives
Unchecked.defaultof<_>
andArray.zeroCreate
.
The following types permit default initialization :
- Any type that satisfies the nullness constraint.
- Primitive value types.
- Struct types whose field types all permit default initialization.
5.4.10 Dynamic Conversion Between Types
A runtime type vty
dynamically converts to a static type ty
if any of the following are true:
vty
coerces toty
.vty
isint32[]
andty
isuint32[]
(or conversely). Likewise forsbyte[]
/byte[]
,int16[]
/uint16[]
,int64[]
/uint64[]
, andnativeint[]
/unativeint[]
.vty
isenum[]
whereenum
has underlying typeunderlying
, andty
isunderlying[]
(or conversely), or the (un)signed equivalent ofunderlying[]
by the immediately preceding rule.vty
iselemty1[]
,ty
iselemty2[]
,elemty1
is a reference type, andelemty1
converts toelemty2
.ty
isSystem.Nullable<vty>
.
Note that this specification does not define the full algebra of the conversions of runtime types to static types because the information that is available in runtime types is implementation dependent. However, the specification does state the conditions under which objects are guaranteed to have a runtime type that is compatible with a particular static type.
Note: This specification covers the additional rules of CLI dynamic conversions, all of which apply to F# types. For example:
let x = box [| System.DayOfWeek.Monday |]
let y = x :? int32[]
printf "%b" y // true
In the previous code, the type System.DayOfWeek.Monday[]
does not statically coerce to
int32[]
, but the expression x :? int32[]
evaluates to true.
let x = box [| 1 |]
let y = x :? uint32 []
printf "%b" y // true
In the previous code, the type int32[]
does not statically coerce to uint32[]
, but the
expression x :? uint32 []
evaluates to true.
let x = box [| "" |]
let y = x :? obj []
printf "%b" y // true
In the previous code, the type string[]
does not statically coerce to obj[]
, but the
expression x :? obj []
evaluates to true.
let x = box 1
let y = x :? System.Nullable<int32>
printf "%b" y // true
In the previous code, the type int32
does not coerce to System.Nullable<int32>
, but the
expression x :? System.Nullable<int32>
evaluates to true.