Maybe and Null
Variables may not contain the value null
by default. This is true even for class- and actor types
that are represented by a reference that could technically contain null
. Instead, Basic Storm
requires using the type Maybe<T>
, or its shorthand T?
, to represent a type that may contain
either the type T
or the value null
. Since Maybe<T>
is a separate type, it is possible to
create Maybe<T>
for all T
in the system, including types that are not reference-types.
The main benefit of this approach is that the language is aware of all variables that may be null
,
and can require the programmer to explicitly check them at appropriate points. In particular, the
class Maybe<T>
only contains members for checking if an element is present (empty
or any
).
There is no way to access the element inside the Maybe
type.
Instead, elements in a Maybe<T>
-type must first be casted into a non-maybe type, T
, by using a
weak cast. Weak casts are described in full in the section on type
conversions, but a brief example is provided below for completeness.
To check whether a Maybe
type contains an object, one can simply use the any
or empty
members
of the Maybe
type as follows:
void fn(Int? x) { if (x.any) { // 'x' was not null. // We can still not access its contents... } }
Using a weak cast, we are also able to access the element if it was not null:
void fn(Int? x) { if (v = x) { // 'x' was not null. print(v.toS); // We can access it now! } }
In this particular case, we could simply write if (x)
, and use x
in the body of the if
statement. More details are available in the type conversions section.
Basic Storm is able to automatically convert from non-Maybe
types into Maybe
types in many
cases. For example, the following function will work as expected:
Str? fn() { "fn"; // Automatically casted to Str? }
In some situations, it might be necessary to manually cast to Maybe<T>
. This can be done either by
calling the Maybe<T>
constructor, or by using the ?
prefix operator. One example where automatic
casts fail currently in is inside if-statements:
Str? fun(Bool c) { if (c) { fn(); // calls 'fn' from the above example. } else { "fun"; } }
The code above will fail to compile since the if
statement does not consider Maybe<Str>
and
Str
to be related. The issue can be solved either by explicitly returning the results with the
return
statement. It might also be solved by an explicit cast in the second case:
Str? fun(Bool c) { if (c) { fn(); } else { ?"fun"; // or Str?("fun") } }
Null
Variables of the type Maybe<T>
are initialized to null
. Basic Storm also provides the keyword
null
as a shorthand for Maybe<T>()
or T?()
. The null
keyword is a bit special. Since it is
not possible to infer the exact type of a null
value (i.e. the type is Maybe<T>
for any T
),
it needs to be used in a context where Basic Storm can infer the type. For example, the following
situation works as expected since the function needs to produce a Maybe<Int>
as a result:
Int? fn() { return null; }
However, the following would produce an error, since we asked Basic Storm to infer the type of the variable:
void fn() { var x = null; // Will prouce an error. }
Representation of Maybe<T>
The Maybe<T>
has two implementations, depending on whether T
is a value, or if it is a class or
an actor. For classes and actors, Maybe<T>
simply contains a reference to the object. In contrast
to other references in the system, the one stored inside Maybe<T>
might be null
. For value
types, Maybe<T>
stores a copy of the value. After the value, it stores a boolean value that
indicates whether or not it represents null
. As such, Maybe<T>
for classes does not incur any
extra memory, while Maybe<T>
for integers incurs an extra boolean of overhead (which typically
consumes 4 or 8 bytes due to alignment requirements).