Today an issue was raised on Discord that caught my attention. As you might know, Scala supports a kind of flow typing for nullable types, which is why this works:
def emptyOr(x: String | Null): String =
if x ne null then
x // x is String here, not String | Null
else
""
But as soon as any type is involved that isn’t a subtype of AnyRef, it no longer compiles:
def zeroOr(x: Int | Null): Int =
if x ne null then
x
else
0
//-- [E008] Not Found Error: -----------------------------------------------------
// | if x ne null then
// | ^^^^
// | value ne is not a member of Int | Null.
An alternative would be to use != instead of ne, but then it works for neither of the two examples above because a CanEqual[String | Null, Null] or CanEqual[Int | Null, Null] is not available.
Values of types String | Null and Null cannot be compared with == or !=
How do we fix this? Do we just need a given CanEqual[Any, Null] in the standard library?
scala.Predef already defines an ne extension method for AnyRef | Null, I guess it would not be a stretch to define one for Any | Null
I have tried this in Scala 3.7.2, which includes that patch.
Any | Null is equivalent to Any, so we’d have to live with the fact that we can now call eq on anything including non-reference types. But I can’t think of a better solution either, my CanEqual suggestion from above has the same problem.
@sjrd said on Discord that null tests shold be done with ==/!= rather than eq/ne, and for that we would need the CanEqual instance. But we could also just provide both CanEqual and eq/ne. I think of == as value equality and eq as reference equality, and since we’re testing for equality with the null reference, I personally prefer eq, but I’m not too dogmatic about it.
There is one case that null tests are done via == in scala.
val data: Null | Int = ???
data match
case null => 13
case _ => data
In pattern matching, the case null is checked via ==. And if we check using case _: Null then it won’t work since Null is not a runtime type. Maybe when explicit-nulls enabled, we can make csae _: Null a special case for type checking just via eq null or == null (I don’t know which is better)