Explicit Nulls in Scala 3
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
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 int
or 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
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 null
and 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|Nullval 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 v
is 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