Thatâs actually useful. Even if things are declared with non-null types they can still be null at runtime because they might not be initialized. So, a test like null == s where s is a String is sometimes necessary and it would be annoying if it was not allowed.
In my humble opinion, any access to an uninitialized field is a bug. It is in the same category as accessing an array out of bounds. This isnât something that one should need to branch on, and I wouldnât let any code that does this pass code review.
And regarding the original issue, I donât think itâs a real problem because while x == null doesnât compile, x eq null works fine, and it makes more sense too because checking for null is a check for reference equality, not for value equality, which is what == is meant to be used for.
There is only one potential issue here: many users donât know eq because itâs not something that is needed very often. If this is of real concern, it can be fixed with a simple compiler hack. When the compiler sees an == null check and a suitable CanEqual cannot be found, the error message should suggest trying eq.
Not even in an assert(s != null)? In code that you donât control fully? I agree eq/ne would be an alternative, but we want to make it easy and straightforward to write such asserts. Someone who adds such an assert in a desperate debug session would not appreciate the technicalities of CanEqual here.
Well, assert is a special case because it is a tool specifically made to detect bugs at run-time. In pretty much all other cases, I donât think one should ever branch on a condition that can only ever be true when thereâs a bug in the program. And adding a language feature that is always available but whose only legitimate use is inside an assert statement doesnât seem reasonable to me, especially given that eq and ne work just fine.
Again, I think a compiler hint to use eq or ne is fine.
We had many issues with language.strictEquality before; unfortunately, we couldnât find anyone who is actively maintaining this feature and discuss the expected behaviour.
Hence, the current behaviour of explicit nulls is based on rules without strictEquality.
After thinking about this again, I have changed my mind. eq and ne are not suitable for testing against null because those are only available on AnyRef. This makes sense at first glance, but unfortunately there are types in Scala that are not subtypes of AnyRef but can still be null, like Int | Null. That means that neither == nor eq can be used to perform a null check on this type, so another solution is needed.
It was proposed to add CanEqual[T | Null, Null] by default when the explicit-nulls feature is used, but due to contravariance thatâs just the same as CanEqual[Any, Null], allowing many meaningless checks.
So Iâve had a different idea. == null and != null are already treated specially by the compiler when explicit-nulls are turned on (flow typing). So letâs just make it a little bit more special: == null and != null donât require a CanEqual instance; instead, the other sideâs type must be a supertype of Null. This neatly allows comparisons against Int | Null while disallowing "null" == null.
The issue with this is that expression do not really have âa typeâ (in Scala), instead they have a tower of gradually more general types (or maybe a latice or something, but tower gives a clearer mental image)
And this tower will always include a nullable type: T | Null
And âthe most specific type for this expressionâ is not necessarily well defined, there are cases where an expression can have multiple valid types that are not subtypes of each other:
Array(1, 1, 1) // both Array[1] and Array[Int] (and many other types)
No, thatâs not an issue. We have other cases in the compiler where we check for similar things, notably the strictEqualityPatternMatching feature in Scala 3.8. It checks that the scrutineeâs type must be a supertype of the patternâs type and the new rule wonât kick in otherwise. By your logic, that shouldnât be possible because every scrutinee has type Any, among others, meaning itâs always a supertype of the patternâs type.
There are also other cases, like this one:
def foo[A](a: A)(using Null <:< A) = ()
foo(42) // error: Cannot prove that Null <:< Int
foo(42: Int | Null) // OK
So this kind of subtyping check is possible to do.
This proposal may lead observers to ask the question why does strict equality not allow âanyâ == comparison where LHS <:< RHS or the other way around - and I guess the answer is because equals may not be defined symmetrically, or even nonsensically in general - but the == operation is hardcoded to check for null if possible - so this is more sound