Skip to content

Exploration: Nullables

Matt Basta edited this page Jan 24, 2016 · 2 revisions

Proposal

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.

Benefits

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.

Arrays

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

Casting

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

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;
    }
}