Built‐in types - IS4Code/Sona GitHub Wiki
Each code element capable of storing a value has an associated type that indicates the allowed runtime type of the values stored in it. This type may be stated explicitly, but it may also be inferred partially or fully from the usage of the element, and therefore can be in many cases omitted completely.
If a named code element needs to be assigned a type manually, it usually goes after it, separated using as
:
// Explicit type
let a as int = 30
// The type is inferred from the value
let b = 30
There is no difference at runtime between these two variables ‒ in the latter case, the compiler can infer that the type is also int
from the value assigned to the variable. Thanks to the Hindley–Milner type system powering F#'s type inference, explicit type annotations are necessary only in edge cases when the types are too complex to be inferred, or for public APIs where the types cannot be inferred from usage or should be more specific than what is inferred.
When a part of a compound type is supposed to be inferred, it is usually done so by omitting it completely, including its surrounding syntax. For example, an arbitrary type T
can be inferred in T[]
(an array of T
) just by writing []
alone, from list<T>
by list<>
, or from function(T)
(a function taking a single parameter of type T
) simply by writing function
.
There are several categories of built-in types:
Some of these types are configurable via pragmas, for example to switch between a class
and struct
implementation.
There are also two special type operators, and
and or
, used to express nullability and conjunctions.
A type T
may be expressed as null-accepting using the syntax T or null
or null or T
. This restricts T
to be a reference type (value types are made nullable by using Nullable<T>
) and indicates that null
is a supported value for the type.
A special type-inferred version of this syntax is also available: null or else
. This indicates an arbitrary null
-supporting type which is derived from the context.
Two or more types may be connected using the and
operator to indicate a type that is supposed to be derived from both of them. For example, IConvertible and IFormattable
indicates an arbitrary type that supports both conversion and formatting, whose identity is derived from the context.
A special version of this syntax with only one type is also available: T and else
. This indicates an arbitrary type that derives from T
, but may be restricted to a more specific type based on the context. This is useful mainly for generic functions, such as when an argument is supposed to be constrained to an interface type but its concrete type is still necessary to track, or when a function is supposed to retrieve an arbitrary type dynamically:
// A source of objects, implementing a common interface
let data as Dictionary<string, IMyInterface> = ...
// Returns an optional instance of the interface but subtyped to the requested type
function get_dynamic(name as string) as (IMyInterface and else)?
// Attempt to narrow the instance to the requested type, or return `none`
return narrow? data[name]
end
// Get the concrete implementation
let a as MyImplementation? = get_dynamic("property")
There are several types that are identified using a keyword and correspond to a .NET runtime type:
.NET type | Keyword |
---|---|
Boolean |
bool |
SByte |
int8 or sbyte
|
Int16 |
int16 or short
|
Int32 |
int32 or int
|
Int64 |
int64 or long
|
Int128 |
int128 |
IntPtr |
nativeint |
Byte |
uint8 or byte
|
UInt16 |
uint16 or ushort
|
UInt32 |
uint32 or uint
|
UInt64 |
uint64 or ulong
|
UIntPtr |
unativeint |
BigInteger |
bigint |
Half |
float16 or half
|
Single |
float32 or single
|
Double |
float64 or double or float
|
Decimal |
decimal |
Char |
char |
String |
string |
Object |
object |
Void |
void |
Exception |
exception |
In expressions, these types must be followed by either a single expression (optionally in parentheses) or function call arguments, resulting in a conversion or construction operation, respectively.
The unit
type is a special type with only one value ‒ unit
. This type is used in all locations where a type is required but its value is meaningless, such as when returning from a function without a value.
An unrelated use of the unit
keyword is also to define and refer to units of measurement.
Function types describe functions, declared using the function
keyword:
function_type:
'function' ('(' parameters_tuple_types (';' parameters_tuple_types)* ')')? ('as' type)?;
parameters_tuple_types:
type? (',' type)*;
All parts of the syntax except the initial keyword are optional, and are inferred from usage if omitted. Aside from that, the usage matches the syntax when declaring functions:
function f(arg as int) as float
...
end
let fcopy as function(int) as float = f
Like in function expressions and declarations, an empty parameters tuple indicates unit
, not an inferred type. This means that function()
is the same as function(unit)
and function(;)
is the same as function() as function()
, not function as function
.
Tuples aggregate several values (of possibly distinct types) together, in a particular order:
let t as (int, char, bool) = (20, 'x', true)
Tuples are constructed by grouping two or more values in parentheses, separated by commas. The built-in tuple type is likewise identified in the same way.
tuple_expression:
'(' ('as' ('class' | 'struct' | 'new') ';'?)? expression (',' expression)+ ')';
tuple_type:
'(' ('as' ('class' | 'struct') ';'?)? type? (',' type?)+ ')';
By default, the tuple syntax identifies a value type (struct
), implemented by System.ValueTuple
. This is however possible to change by using a specialized syntax, or #pragma tuple
:
#pragma tuple class
// Now implemented as System.Tuple, a reference type
var t as (int, int) = (1, 2)
// Explicit syntax, same type
t = (as class; 5, 10)
// Implemented as a value type, an error
t = (as struct; 5, 10)
A value tuple cannot be assigned to a reference tuple, because such an assignment would need to form a new identity for the tuple, and could lead to surprising results from code that relies on reference equality of tuples. Assignment is however possible in the opposite direction (i.e. reference tuple to value tuple) because the target type does not distinguish identity.
In expressions, as new
can be used to differentiate a tuple from other pieces of similar-looking syntax, such as function arguments. The spread syntax (..
) can also be used to place the contents of one tuple into another.
In a type, individual tuple elements can be inferred simply by omitting them, thus (,)
identifies a pair of arbitrarily-typed values.
Records are similar to tuples, however they are not positional, instead members are identified by name. Anonymous records do not need to be declared and are determined solely by their members.
let r as {M as int, N as bool} = {as new; M = 3, N = true}
Like usual, the record type can be inferred from the usage, including the types of its members:
function f(r as {M})
return r.M
end
Anonymous records are constructed by using curly brackets to group members. In expressions, the initial as
is necessary to distinguish them from normal records.
anonymous_record_expression:
'{' 'as' ('class' | 'struct' | 'new') ';'? name '=' expression (',' name '=' expression)* '}';
anonymous_record_type:
'{' ('as' ('class' | 'struct') ';'?)? name ('as' type)? (',' name ('as' type)?)* '}';
By default, records are reference types, however this can be changed via #pragma record
or by using the specialized syntax:
// Implemented as a reference type
var r = {as new; M = 1}
#pragma record struct
// Now as a value type, an error
r = {as new; M = 2}
Like with tuples, value type-based anonymous record cannot be assigned to a reference type-based anonymous record, for the same reasons.
An option type expresses optional presence of a value. It is created using two constructors: some value
(when a value is present) and none
(when a value is absent).
// Option initialized to some value
var o as int? = some 10
// Reset to no value
o = none
The option type is indicated by the ?
suffix.
option_type:
type? '?'.
The suffix can also be placed alone, indicating an optional value with its concrete type inferred from context.
By default, options are implemented using the voption
type in F#, a value type. This can be changed via #pragma option
:
// Implemented as a value type
var o = some 20
#pragma option class
// Now as a reference type, an error
o = some 30
The reference and value-typed option types are not mutually compatible.