Note: Option values may be created and pattern matched. Most sugar is not working.
The Option type is useful when we want to express the idea that we may or may not have a value of a certain type. These are also called nullable types, since Null represents the absence of a value. Nullable values are so useful that many kinds of syntactic sugar exist to make them easy to work with.
Creating Nullable Values
Let's declare and initialize a variable that might hold an integer:
mut maybeInt ?i32 = 5
Notice that the type specifies a question mark before the integer type. The question mark signifies this is an Option type. It is a syntactic abbreviation for Option[i32].
The value of 5 is automatically coerced into its nullable integer equivalent. It would also be possible to specify this explicitly using Some[i32] or its shorter inferred version: Some.
To initialize this variable with the null value:
mut maybeInt ?i32 = Null
Again, Null is an inferred abbreviation for Null[i32]. The abbreviation is only possible when the context makes it clear what sort of Option type we are working with. Thus, this is ambiguous and therefore an error:
mut maybeInt = Null // **ERROR**: nullable what?
Nullable References Nullable references are optimized so they take no more space than regular references. The Null reference value is just a non-addressable address (typically, 0).
Unwrapping Nullable Values
We cannot work with a nullable value in the same way we would a value of its enclosed type. Imagine trying to add some number to a nullable integer (for example: maybeInt + 4). How should it perform the addition when maybeInt is Null? In the face of this uncertainty, the compiler views it as unsafe and will emit an error.
To safely work with a nullable value, it must first be unwrapped. This means performing pattern matching on the nullable value to determine whether it holds a value or Null. Based on which it is, we can then act one way in one case and another way otherwise.
Explicit pattern matching is versatile enough to handle any unwrap-and-act scenario, but the resulting code may be undesirably verbose. Several operators offer succinct ways to work with nullable values.
Force unwrap, treat Null as an exception
One approach is to expect the value to not be Null, and treat it as an exception if it is. To handle a nullable value this way, follow the nullable value with the ? operator.
imm val = maybeInt?
If maybeInt is not Null, then val will be the unwrapped integer value inside.
An exception will be thrown if maybeInt is Null. This exception can be caught and handled if a catch null handler is provided. If one is not specified, the exception will panic.
Give Null a default value
An alternative approach to unwrapping involves specifying a default value use when the nullable value is Null. This form of unwrapping uses the or operator:
imm val = (maybeInt or 0) * scale
val will use the value of maybeInt if it is not null. Otherwise it will use 0.
The value to the right of the operator cannot be a nullable type, and its type must match that of the wrapped type for the nullable value on the left. The expression for the default value will only be evaluated when the nullable value is Null.
Null propagation on safe method calls
A third operator applies when we want to perform some operation on a nullable value when it is not Null, but evaluate to Null when it is. This approach uses the ?. operator, a variation of the dot operator used to perform methods or access fields:
imm x = maybePoint?.dist()
If maybePoint is not Null, then x is a nullable value that wraps the calculated distance of that point. However, if maybePoint is Null, then x will also be Null. In effect, we are performing a method or accessing a field if it is safe to do so, and propagating Null otherwise. Unlike the previous two operators, the result of using ?. is always a nullable value.
This operator may also appear on the left hand side of an assignment statement, and will bypass the assignment altogether if the nullable value is Null.
Conditional Variable Specialization
Suppose we have a nullable value stored in a variable, and we just want to check whether it is not Null only once. If it is not Null, we want to be able to work with its unwrapped value using the same variable name without having to constantly reaffirm it is not Null. This is surprisingly easy to accomplish:
if maybePoint: imm x = maybePoint.x imm y = maybePoint.y imm dist = maybePoint.dist
When a nullable value is used in a conditional expression, it evaluates as false if Null and true otherwise. So, the above block will be performed whenever maybePoint is not Null. When this not Null check is using a variable, as it is in this case, a local immutable shadow of that variable is implicitly created at the top of the block which contains a copy of the unwrapped value inside maybePoint. Since this shadow variable contains an unwrapped value, it can be directly used within that block as a Point value. The use of this technique means, however, that the original variable is not accessible nor mutable within the block.
As an extension of this principle, if the conditional expression follows such a check with additional conditions after the and operator, the shadow variable may also be used in the remaining condition:
if maybeInt and maybeInt < 10: scale *= maybeInt
A conditional can also check for the Null value:
if !maybeRef: // do some error recovery actions
The block is performed when maybeRef is Null. No shadow variable is created for maybeRef at the top of the block, as we have no value to unwrap.
If none of the above convenient approaches apply to your situation, just make use of straightforward pattern matching:
if imm integer = Some[maybeInt]: scale *= integer else: // Some recovery stuff
When maybeInt is not Null, its value is unwrapped and copied into the variable named integer, where it can then be used inside that block. If it is Null, then the alternative block is performed.