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 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 |
Int32 |
int32 or int
|
Int64 |
int64 |
Int128 |
int128 |
IntPtr |
nativeint |
Byte |
uint8 or byte
|
UInt16 |
uint16 |
UInt32 |
uint32 or uint
|
UInt64 |
uint64 |
UIntPtr |
unativeint |
BigInteger |
bigint |
Half |
float16 |
Single |
float32 |
Double |
float64 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. When followed by a single value, some of these types have different semantics than just calling the type's constructor:
- Numeric types,
bool
,char
, andstring
attempt a conversion of the value to the target type. This conversion may result in an exception on failure, which can be amended by using?
‒ for example,int? x
attempts a conversion ofx
toint
and results in a value of typeint?
indicating whether the conversion succeeded. -
object
boxes the value (casts it toobject
), equivalent to callingbox
. -
void
evaluates the expression but results inunit
, equivalent to callingignore
.
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.