You can do (currently, but not with the proposed change): T & (AnyVal | AnyRef)
Since the latter is a type which contains everything except null, taking the intersection with any T removes null from it.
(null is currently the only value for which this is possible, I don’t think you do the same for “the type of all integers except 2” for example)
FWIW, I also agree that Null <: Any is wrong but of course that’s impossible without completely nuking the type hierarchy. Null IS special, its not a reference OR a value, its the absence of a meaningful value. I’d prefer if Null NEVER entered a type unless explicitly added but that’s impossible given Null <: Any.
For inside the compiler, I assume that Null will always have special casing anyway in explicit-nulls, so I’m not sure what we’d be saving.
For teachability, yes, Null SHOULD be segregated from the rest of the types! Make it very clear that in Scala, we don’t use null unless writing high performance code or interoping with Java.
My ideal type system would remove Null entirely and instead use an alternate system for tracking nullability. I think(?) we are really the only typed language that insists on having Null as a type in the type system (likely an artifact of Null <: AnyRef being the base). C#, a retrofitted language, uses ? like Kotlin. jSpecify of course uses these kind of augmenting types as well.
Java interop is of course a concern, but we shouldn’t compromise our type system to fit Java. We design the type system we want, and then we adapt Java to it to the extent that we can.
Some things can’t be expressed. To give a random example off the top of my head, we can’t express constructors whose type parameters differ from their enclosing class.
Technically, yes. However that’s useless, because there is nothing that you can do with a T & (AnyVal | AnyRef) that you can’t do with a T. So for all practical purposes, using T is just as good.
This is a misunderstanding of what explicit-nulls is all about. Java-like systems (jSpecify, Kotlin), treat null out of their hierarchy, yes. They make it special, with dedicated syntax and typing rules. Because they don’t have union types. Scala does. Scala does not need to make null special, and that’s a good thing.
The design for explicit nulls in Scala is that null is not special anymore. Null is a regular class with a singleton instance (like Unit with ()). There are fewer rules mentioning Null in the type system with “explicit nulls” than without.
Aside: There are a number of special-cases of null in the flow typing, which is somewhat outside of the type system per se (flow typing does not affect subtyping, for example). That’s a compromise in the name of backward compatibility. It’s not a good thing. In a fresh system, pattern matching decomposing union types would be enough to deal with any X | Null. This is a trend I’ve seen in many languages with some form of flow typing: they all do because of backward compatibility with a previous version that had a less strict type system (often: completely dynamic).
Not at all. I think you’re focusing too much on type systems that don’t have first-class union types. TypeScript, for example, has union types and does have a null type for the null value. Scala’s design is not unique here. Just because Kotlin and jSpecify have chosen one design does not mean that it is the correct design for Scala, because they don’t have union types.
Typescript also has a way to cast away null at the type level
I’m fine with this proposal as long as there is a way to cast away null at the type level (and preferably also a way to specify a non nullable bound, but that can possibly be emulated just with casting away null). The KEEP proposal for definitely non-nullable types includes a problem with java interop that required them to implement a very basic form of intersection types that are user visible (kotlin always had intersection types, they were just non-denotable).
public interface JBox {
<T> void put(@NotNull T t);
}
The above code, when overriden, would require some way to specify that T can’t include null, which Kotlin couldn’t do at the time. We currently can do that with intersection types, but we may not be able to do that in the future.
Of course right now Scala processes the @NotNull annotation by skipping that type and not transforming it at all. This is correct for most types, but like in my previous post, jSpecify says “the difference is significant for intersection types, type variables, and union types”.
Again this could be solved in a future proposal, but for me the question is “why change what works” as it will cause more require work in the future.
I do agree that null does make sense as a value, its more or less identical to Unit, we just assign it special meaning in the non-explicit nulls case. I’m not strictly opposed to this proposal, just consider interop in the future. It could end up being a large headache having to define a NonNull type in the future.
Maybe Scala could translate that as
type NonNull[A] = NotGiven[Null <:< A]
trait JBox {
def put[T](t: T)(using erased NonNull[T]): Unit
}
type NonNull[A] = NotGiven[Null <:< A] trait JBox { def put[T](t: T)(using erased NonNull[T]): Unit }
This is incorrect and changes the meaning of the code. This would be no different than this code in java:
interface JBox {
<T extends @NotNull Object> void put(T t);
}
which is not the same as the original example. The OG example allows nullable types as input.
The point explicitly is to cast away null, or “MINUS_NULL”, as jSpecify calls it.