Optional types can have all values of an underlying type plus an additional value "none". The
underlying type can be any type including object, value or struct types.
optional_type
: type "?"
;
For all object types T and U if T <: U then T <: T? <: U?. Given an optional type X?
there is an implicit conversion to further nested types X??, X???, and so on. For value and
hybrid types V there is an implicit conversion from V to V?. If there is an implicit or
explicit conversion from V to W then there is a corresponding lifted conversion from V? to
W?.
The special value "none" is used to represent when an optional type does not have a value. The
value "none" has the type "never?" thus it can be assigned into any optional type.
To conditionally operate on an optional value, use a pattern match expression. Other ways of
checking for "none" are possible but not preferred.
let x: int? = ...;
// Idiomatic way of checking for `none`
if x is let y?
{
// y is the value of x
}
// Not Recommend unless part of a larger match
match x
{
y? => ...,
none => ...,
}
// Not Recommend
if x =/= none
{
// Can't directly use the value of `x` in this block
}
The coalescing operator ?? allows for the replacement of none with another value. If the
expression to the left of the operator evaluates to a value other than none then the result of the
coalescing operator is that value and the right-hand expression is not evaluated. Otherwise, the
result is the result of evaluating the right-hand expression. Note that this is a short circuiting
evaluation. The coalescing operator can be combined with assignment as ??= so that the left-hand
side is evaluated and if it is none then the right-hand side is evaluated and assigned into the
left-hand side. For nested optional types, the coalescing operator evaluates through multiple
layers. Thus for a variable x1: X?? and expression x2: X the expression x1 ?? x2 has the type
X and evaluates to the left-hand side if x1 is not none and to the right-hand side otherwise.
TODO: there needs to be a way to evaluate only one layer. Perhaps ?? is one layer and ??* or
??? is multiple.
Members of optional values can be accessed using the conditional access operator x?.y. This
operator evaluates the left hand side. If the left hand side evaluates to none, then the right
hand side is not evaluated and the result of the expression is none. Otherwise, the member is
accessed and evaluated. Note that this is a short circuiting evaluation so that x?.y.z() would
prevent the method z from being called if x were none, rather than simply evaluating x?.y to
none and then attempting to call z() on it. As with the coalescing operator, this can evaluate
through multiple layers of optional type.
For a type T with operators defined on it, many of those operators are lifted to operate on the
type T?. The operators that can be lifted fall into three categories based on how they are lifted.
The three categories are the arithmetic operators, comparison operators, and logical operators.
Operators are lifted to multiple layers of optional type with a none at any layer being treated
the same. This means that even when operating on multi-layer optionals the result will always be a
single layer optional type.
The arithmetic operators +, -, *, /, +=, -=, *=, /= (both unary and binary) are
lifted so that if either argument is none the operator result is none.
The comparison operators ==, =/=, <, <=, >, >= are lifted to that none == none but the
comparison of none to any other value results in none. Thus none == none, none <= none, and
none >= none are all true since none is equal to itself. Likewise none =/= none, none < none, and none > none are all false. Comparison on optional types is a proper partial order where
none is comparable to itself but not to any other value.
The logical operators not, and, or, xor, implies, iff are lifted to operate on bool?
according to three-valued logic where the value none represents an unknown truth value. They
follow the Kleene logic. The truth tables are given below. The logical operators are still short
circuiting, however the none value can influence whether the second argument is evaluated. For
example, it is still the case that true or x will not evaluate x, but none or x must evaluate
x to determine whether the result is none or true.
a |
not a |
|---|---|
false |
true |
none |
none |
true |
false |
(symmetric cases omitted)
a |
b |
a and b |
a or b |
a xor b |
a iff b |
|---|---|---|---|---|---|
false |
false |
false |
false |
false |
true |
false |
none |
false |
none |
none |
none |
false |
true |
false |
true |
true |
false |
none |
none |
none |
none |
none |
none |
none |
true |
none |
true |
none |
none |
true |
true |
true |
true |
false |
true |
a |
b |
a implies b |
|---|---|---|
false |
false |
true |
false |
none |
true |
false |
true |
true |
none |
false |
none |
none |
none |
none |
none |
true |
true |
true |
false |
false |
true |
none |
none |
true |
true |
true |
The if and while expressions both have a condition which is a bool or bool?. If the
condition evaluates to true then the body is evaluated. if/else expressions only accept
bool. This is because the value true is the only true value. This is what distinguishes the
bool? logic as Kleene logic instead of Priest logic. That is a value of none a while to exit.
Multi-layer conditionals like bool?? cannot be used as a condition and must be transformed to
bool or bool?.
Optional types are a wrapper for a bare type that exposes the capability of the underlying type. Thus own T? is logically own (T?) and can be used as a generic argument where an own type is expected.
By default, the constraints where T: object, where T: value, and where T: struct do not allow
optional types. To allow optional types use where T: object?, where T: value?, and where T: struct? to allow optionals of the various kinds. Likewise, where T: Trait does not allow optional
types unless there is already a constraint that allows optional types. To allow optionals, use
where T: Trait?.
Note that where T: drop does permit optional types since the drop operation will automatically
handle optional types.
If optional types are allowed, then multi-layer optional types are allowed and automatically unwrapped through multiple layers when needed.
This section is meant only as guidance how optional types might be implemented. The actual implementation may be anything that implements all language requirements.
For an optional reference type, the bit representation is the same as a reference. The none value
is represented by a null reference. That is, all the bits of the reference are zero.
For all other types T, optional types act similarly to a constant value type Option[T out].
Multiple layers of optional (e.g. T??) can be thought of as nesting (e.g. Option[Option[T]]).
However, the recommended representation is able to represent both a single layer of optional and up
to 255 layers of optional. Internally, they may be represented as the bits of the type T followed
by a single byte. If the value is not none then the bits of the value come first followed by
zero for the byte. If the value is none then the value bits should be zero and the byte contains
an integer indicating at which level the none occurs. That is a none for T? would be 1.
However, for type T?? there are two none values. If the inner T? is none then the byte has
the value 1. If the outer optional of (T?)? is none then the byte value is 2. Thus this
representation can represent up to 255 layers of nesting since the maximum value of byte is 255.
If it is necessary to represent more than 255 layers of optional, then this representation can be
nested. Thus if this representation is like a value type Multi_Option[T] then more than 255 layers
can be represented by Multi_Option[Multi_Option[T]].
It should be noted that for a reference type R, the type R? is not a reference type. It would
need the above representation. Thus for the type R??, the inner optional being none is
represented by a zero reference, while the outer optional being none is represented by the value
1 in the byte following the reference bits.
Optional booleans can be further optimized with a special case type. For this, false is zero,
true is one and values above one are used to represent none at various layers of nested optional
types. This makes the total size of most optional booleans still one byte and means that implicit
conversion from bool to bool? is a no-op.
The reasons this is a good representation are as follows:
- Reference types are optimized for the simple case and have the same representation reflecting the
fact that
R <: R?for reference types. - Implicit conversion to optional requires only appending a zero byte.
- Implicit conversion to additional layers of optional typically is a no-op.
- A single byte provides more than enough layers of optional for typical cases, but can also be fit
within the space of many value types that contain values smaller than the native word size (i.e.
if a value type contains a
boolorbytethen it will already have a representation that isn't an even multiple of the native word size. Thus it will need to be padded out for alignment. The optional version of this type can then place the additional byte within that padding so that the aligned size of the optional type is no bigger.)