-
Notifications
You must be signed in to change notification settings - Fork 1
Exploration: Nullables
null
is a dangerous tool in a language, but it provides extensive value. By allowing object references to be null
, the lack of a value can be represented. The return of a lookup to a map can be null if the value does not exist, rather than throwing an exception (eliminating a has()
method call or the need for an in
operator). Returning errors (as in Node or Go) is only possible if the error object can be null, indicating no error has occurred.
Null values pose significant challenges:
- Accessing a member or calling a method of a null object
- Calling a function that is null
- Taking a subscript of a null object
Additionally, garbage collection performance is potentially degraded, as values must be checked to see whether they are null before they can be traversed (or reference counted). Runtime performance is similarly degraded, as null guards must be inserted into the compiled code.
The token ?
will be added. It will be allowed after any type name:
Foo?:x = null;
func int:process(Foo?:toProcess) {
array<Obj?>:bar = foo();
Obj<int>?:bar = foo();
Foo<Bar?>?:bar = foo();
- ...
?
is not be allowed in object declarations after generics.
object Foo<T?> {} # Invalid!
A type suffixed with ?
is considered "nullable". A nullable type may contain null
. Non-nullable types may never contain null
.
Primitive types may not be marked as nullable:
int
float
bool
str
uint
byte
An isnull
postfix unary operator will be added. It will have the same precedence as as
.
var x = foo();
var y = x isnull;
isnull
returns a bool
indicating whether the value passed to it is null
. Any value of any type may be passed to isnull
, though the output for types that cannot be null (int
, str
, etc.) will always be false
. This is useful for scenarios where the value given is inferred and not necessarily known to the developer.
Because only nullable types may contain null
, guards and GC checks may be removed from variables for non-nullable types. Additionally, runtime safety is improved, as null reference errors cannot occur with the default (non-nullable) types.
The array constructor provides no means of specifying a default value. Currently, the contents of an array containing non-primitives created with the array constructor is undefined. As such, it will be changed to throw a compile error for non-nullable types.
var x = new array<Foo>(); # Compile error
var x = new array<Foo?>(); # Valid
var x = new array<int>(); # Valid
Because BType does not include implicit type casting, nullable types may not be used easily in certain scenarios. However, some implicit typecasting for nullable values will be added. Implicit casting will occur in the following circumstances:
- When accessing a member of a nullable type
- When calling a method of a nullable type
- When taking a subscript of a nullable type
When casting to the non-nullable version of a type, the value will be compared to null
. If the value is equal to null
, an exception will be raised.
An exception to this rule will be made for equality testing. Nullable types may be compared to their non-nullable equivalent type. If the nullable side of the binop is null
, no exception will be thrown. This is functionally equivalent to implicitly casting the non-nullable side to its nullable equivalent.
Non-nullable types will always be implicitly cast to their nullable variants when used in those locations.
Foo:x = fooFactory();
Foo?:y = null;
y = x; # Valid
Generics may use nullable types.
object Foo<T> {
T:member;
}
var x = new Foo<Bar?>();
x.member = null;
x.member = new Bar();
Objects may use isnull
for null
testing.
object Foo<T> {
bool:gotNull;
new(T:foo) {
self.gotNull = foo isnull;
}
}