Explicit Nulls in Scala 3

ayush mittal
5 min readFeb 18, 2021
Photo by Ben Hershey on Unsplash

Scala 3 has an optional feature which changes the Type hierarchy when enabled. Its called “Explicit Nulls” and when enabled via the flag -Yexplicit-nulls the language has a different way of working with null values.

“Null” in current scala hierarchy

scala-type-hierarchy without explicit nulls feature

Above diagram shows where Null type is placed normally in the Scala type system.Its a subtype of every type except the value classes like intor Long . This means that reference types could be assigned null values and were nullable as per the type hierarchy.

val aString: String = null // compilesval aList: List[Int] = null //compilesval aCustomType: CustomType = null //compiles

“Null” in “explicit-nulls” scala hierarchy

scala-type-hierarchy with explicit nulls feature

So, what we see above is that Null is not a subtype of AnyRef but is directly a subtype of Any . That means that all reference types , collection types, Strings and any other custom type cannot be assigned null value.

val aString: String = null // error found Null required Stringval aList: List[Int] = null //error found Null required List[Int]val myType: MyType = null //error found Null required MyType

Above snippets show that we can not explicitly assign a null value to AnyRef types.

Does that mean reference types can never have null values ? Moreover , can we stop checking for null references i.e isMyValue == null ? Not so fast .. There are still cases when reference types can have null values even when explicit nulls feature is enabled.

Unsoundness

Lets look at some examples when reference types can have null values even with -Yexplicit-nulls set :

class Employee:
var name: String = _


val employee: Employee = new Employee
val name: String = employee.name // compiles ; but the name is null

In the above scenario , the var value is by default initialised to nulland even though the type of name variable was String it had null values. Another example :

abstract class Person:
def fullName: String
val nameLength = fullName.length

class RoyalPerson(name: String) extends Person:
val append: String = s"Your Majesty $name"
def fullName: String = append

val person: RoyalPerson = new RoyalPerson("Dave")
//Throws NPE at fullName.length

Let’s look at the above example. fullName.length throws a NullPointerException indicating the null value of fullName. But wait..isn’t fullName a String type which cannot be null?

The reason for the exception is the fact that reference types are initialised to null value by default. Here we are trying to use append before its initialised and pay the penalty.

The two examples above depict what is known as unsoundness with respect to null values. Scala 3 provides an option to catch these unsound use cases. The option is -Ycheck-init which enables safe initialization checks in your code base. If we enable the check in our code we get the following warning :

//warning
Access non-initialized field append. Calling trace:
-> val nameLength = fullName.length
-> def fullName: String = append
val append: String = s"Your Majesty $name"

Working with “Null” values

To work with null we have to be more explicit. If we have to mark a type as nullable, we have to use the union type.

val someString: String | Null = null

Before you can extract the String from this union type you must perform a pattern match or use if/else

val someString: String | Null = ???//use if/elseif(someString!=null)
someString.length
// or use pattern match
someString match
case str: String => //use string
case _ => //null

Scala 3 provides an extension method .nn to cast away nullability. But the method will throw an exception if the value is null. So use with caution!

val stringOrNul: String | Null = ???val string: String = stringOrNull.nn //will throw exception if null

Java Interoperability and UncheckedNull

What happens when we load or use a Java class in Scala. What types would get assigned to Java values which can be null ?

// A java class
public class Person {
String name;
}

Let’s use this class in Scala and checkout the type of name in Scala.

val person = new Person
val name: String|UncheckedNull = person.name

Why does java person.name not return a String and what is this new type? Scala 3 basically converts a java nullable type A to A|UncheckedNull as part of loading source code or bytecode. This patching or conversion is done for members of a class, argument types and return types of methods.

So if we have a Java class like this

class A {
String b;
int c;
String getB();
}

Then its Scala version would be

class A :
val b: String|UncheckedNull
val c: Int
def getB: String|UncheckedNull

UncheckedNull is same as Null for all purposes except that it allows to call methods that are unsafe.

val name: String| Null = ???
name.charAt(1) //error, chatAt is not a member of String|Null
val anotherName: String|UncheckedNull = ???
anotherName.charAt(1) //allowed

If this type was not present then method chaining would become too tedious.

val string = getStringFromJava()
val result =
if string != null then
val tmp = string.trim()
if tmp != null then
val tmp2 = tmp.substring(2)
if tmp2 != null then
tmp2.toLowerCase()

But with UncheckedNull we can simply do

getStringFromJava().trim().substring(2).toLowerCase()

Flow typing

Scala 3 can infer that a type vis not null based on the checks that are done for null in the path that derives v. Let’s understand this with an example

val v: String | Null = ???if(v !=null )
//here v is not null, its a String
v.toUpperCase() //compiles
//v is still nullable in the outer scope
//v.toUpperCase() does not compile

So, once we check that v!=null then the code block inside that scope treats v as a String type. Similar logic holds for checks like assert or for ==null check.

val v: String | Null = ???
assert(v!=null)

//v is now a string type
v.toUpperCase()
.....val v: String | Null = ???if v== null then
//
else
v.toUpperCase() // v is String now

Conclusion

Explicit nulls seems to be a nice step in the direction of making Scala more safe and adds a more functional feel to the language. Turning the feature on in our code bases might require a lot of re-writing. That’s why its not on by default. However upon usage, the end code is more reliable and free from potential NPE’s that we are all so wary of.

Further readings

  1. https://dotty.epfl.ch/docs/reference/other-new-features/explicit-nulls.html
  2. https://dotty.epfl.ch/docs/reference/other-new-features/safe-initialization.html
  3. https://github.com/lampepfl/dotty/pull/5747
  4. https://ayushm4489.medium.com/union-and-intersection-types-in-scala-3-6b5f7e818dc4

--

--