Make Null a subclass of AnyVal under -Yexplicit-nulls

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.

1 Like

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.

1 Like

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.

I guess I missed something, I must have so bare with me, doesn’t this change render explicit-nulls unusable?

How I am supposed to define a function with type parameters which does not include Null once it is under AnyVal? Because in the end I use explicit-nulls so that I have to deal with null only at the border and inside I am “clean”. I.e. at the border I have something like (typically parsing json or the like):

def controller[T<: AnyVal|AnyRef](t: T|Null) = 
  if t == null then throw IllegalStateException("You have to define")
  else service(t)

def service[T <: AnyVal|AnyRef](t: T) = println("t is not null")

controller(externalInput)

If I have to start adding nn everywhere again (or “trust” the consumers of the function to never pass null which of course is risky) then I don’t really see the benefit.

What do I miss?

i think maybe what you’re missing is that after this change Null is just another well-behaving value type and in pure scala there’s no reason to exclude it from generic code any more than you would exclude Int or Boolean

the only 2 cases where there might be a reason to is:

  1. null as sentinel - see sjrd’s superior alternative (ie. allow Null and use a private object as sentinel instead)
  2. interop - see the discussion about a way to exclude null generally, and in the meantime use a combination of using erased NotGiven[Null <:< T] (for Object-only types) and .nn (for broader types)

this is my assessment of the situation. someone pls correct me if i’m wrong

4 Likes

there is a follow up proposal that basically should make those methods on Any that throw NullPointerException instead handle null gracefully when the reciever’s upper bound includes Null (for code compiled with latest compiler) - so therefore no fear of NPE when calling e.g. x.toString or x.getClass (specifically with additional wrapper code only in situations where x: T and Null <:< T)

1 Like

above quote was about rejecting nulls, but let’s re-read it in another context, i.e. why do we need nulls in scala type system at all? idiomatic scala code avoids null usage completely, unless java interop is needed. in other words, null is mainly for interop with java. if we take that position then:

  • if we teach or use pure scala without null-oriented java api usage, then we can skip teaching or thinking about null and it doesn’t really matter where null type is in type hierarchy
  • if we use null-infested java api from scala, then we need to know how java uses nulls anyway, so having a scala null that works very differently from java null is counterproductive here
1 Like

As a side note, I must have been out of my head, explicit-null keeps being beneficial if type parameters are not involved. Things like Int don’t suddenly include Null :see_no_evil_monkey:

For me explicit-null should enable that NPEs are thrown as early as possible i.e. at the boundary, without me needing to insert nn. As soon as I abstract over something which needs to store the value and operates on Any, I loose the possibility to enforce non-null and thus to fail early – I am back at scala 2 where I had to trust the callers behave nicely. This works but IMO enforcement via the type system is way nicer.

As shown in the follow-up thread, getClass doesn’t work and requires a type test, so back at using nn and the like :disappointed_face:

edit my bad, getClass returns jl.Class. Now I see what was meant with null behaves like a normal value.

More as a side question. This change is not a binary break, neither a source (all programs still compile), but it is a behaviour change which can break programs.

Let’s say there is a function which is using a Java map internally (or calls a Java library which uses one etc.):

def foo[T <: AnyVal](key: String, value: T): T = 
  // do something and eventually …   
  internalJavaMap.computeIfAbsent(key, _ => value)

computeIfAbsent is using null as sentinel, The above function did not intend that null can be passed and hence also not documented this behaviour. Now with the change, suddenly not all possible T will make it into the map.

Is there a term for such kind of breaks in a language and maybe more importantly, should such a break target the next major version and since I am at it, is there actually already a road map for Scala 4.0 :smiley:

hmm. good point that the proposal would introduce a possible bug to similar code. but i wonder how much code like that actually exists and that would exhibit the issue

it seems to me unusual (value types only passed to a java collection that natively doesn’t support value types) so probably rare? also, realistically, what would be a source of a null that might blow up this function? pure scala wouldn’t generate a null and interop code that does would have an AnyRef | Null type, values of which wouldn’t be passable to the function without a retyping. is this more of a theoretical problem than a realistic one? idk

your desire for ease of use seems entirely fair. i agree that ease of use in its core use case should be a priority. imo tho the greater priority is that it be a well-behaving part of the (unified) type system as that is the source of all of its problems currently. null isn’t just found in java code or at the boundaries. it flows through scala code, and if it’s outside of the type system it can’t be well reasoned about and chaos is the result. it’s why primitives are in the scala type systems and not in the java one. all values need to be under the type system. null is another such case but a worse one. seeing it as special and thus beyond the normal type system is the reason it’s such a serious source of issues

this feels a bit like the static / typed // dynamic / untyped code argument: bringing everything under a proper type system leads to far less errors in the long run, but older type systems were simplistic and a pain to use and that drove people away from them for a time. so the type system is the priority but ease of use is important so that everyone appreciates the system rather than hating its existence

null currently ruins scala imo cause it looks like it’s in the type system but it kinda just does whatever it wants. i think it’s possible for it be different from java (so that it can stop being a source of serious problems) whilst still being easy to use with java. do you have any specific issues with the proposals that would make it worse in that respect for you?

@daniel :

what are you even comparing?

  • implicit-nulls mode (the only mode in scala 2) to explicit-nulls mode (that requires opt-in in scala 3)?
  • or maybe current explicit-null mode (i.e. the one that is already implemented in scala 3 compiler and used by some scala programmers) to proposed explicit-nulls mode (from this thread)?

the proposal from this thread is a net downgrade from technical perspective for scala users. it:

  • worsens java interop
  • prevents straightforward implementation of nonnull type
  • doesn’t add any new capability
  • doesn’t make null any less of a footgun

from scala user point of view, the only potential advantage this proposal brings is the claimed teachability advantage, but it’s dubious at best, since nulls in scala are used mostly for java interop (and rarely for unidiomatic low-level performance optimizations and that’s it). making java interop worse will make teaching about java interop worse too, and since nulls are mostly for java interop, then they should be taught in context of java interop. otherwise scala programmers should be taught to avoid using nulls. i’ve already written about that in recent posts in this thread.

imo nulls shouldn’t flow through scala code if they don’t need to. scala code should use options and convert between options and null at boundary between scala code and java code. in fact, that’s what every regular scala programmer does. nobody wants nulls flowing freely through scala code.

note that java has java.util.optional type since java 8, but it’s not used as widely as option in scala. java ‘gurus’ (i.e. java specification authors, java trendsetters, java mvps, etc) are telling java programmers to keep using nulls in apis and use optionals only locally. that means null will keep being prevalent in java, so java needs highly effective and user-friendly null handling. also java is strongly backward compatible, so that restricts what java authors can do with null-related syntax.

what should i repeat? i’ve already stated my opinion many times. let’s keep the current type hierarchy under explicit-nulls mode as it’s already implemented in scala 3, instead of changing it according to the proposal from this thread.

2 Likes

can you explain why its needed to exclude nulls in a truly generic context that will also box primitives (other than “yuck!”)? (assuming future compiler will avoid throwing NPE so no manual null guard would be needed)

i assume you’re referring to my nonnull argument, do you? i’m not sure if you do, or you’ve missclicked the wrong reply button :slight_smile: as it sometimes happens. it would help if you quote the exact part of my post.

anyway, requesting non-nullability is important for java interop (and java interop is very important) and that’s mostly it. regular scala code probably won’t (and shouldn’t) bother with nullability at all, except converting between nulls and options on java<->scala boundary and also some very rare cases (like matching on any in current explicit-nulls mode or matching on anyval in the proposed explicit-nulls mode). for java interop, nonnull type helps with modelling java nullability markers as in "MINUS_NULL" and the future of explicit nulls - #9 by tarsa

given that there’s an active work on intergration of first parts of project valhalla JDK 28

JEPs proposed to target — JDK 28 review ends
401: Value Objects (Preview) — 2026/07/30
539: Strict Field Initialization in the JVM (Preview) — 2026/07/30

we should wait a few years to see if this proposal will make java interop noticeably worse (e.g. cause performance degradation or nullness impedance mismatch somewhat akin to Object–relational impedance mismatch - Wikipedia), i.e. whether concerns from "MINUS_NULL" and the future of explicit nulls - #12 by tarsa will be valid or not. tiny differences in generated java bytecode can tank performance as seen on e.g. SIMD implementation and Kotlin checkcast incompatibility :: Gryt

2 Likes

To me it seems like there is broad consensus that “null should only live at the interop boundary”

Why I think there is consensus

For any relevant task, Scala was designed in a way that both makes it hard to use null and makes it easy to use something else.

For example if you want “a value of T or not”:
Using Option[T], you get a lot of tools to do things (deconstructing, map, orElse, …)
And using T | null, you essentially get nothing, you have nn and if, and on top of that toString blows up in your face !
To contrast, Kotling makes it easy to use null, you get x?.foo, x ?? someDefault, x!!, x.toString works, and even the type is made short: T?

So naturally we don’t want to touch null if we don’t have to !


I think the real disagreement here is “How can we ensure we don’t have to worry about null ?”

And there seem to be two camps: “Keep null being weird, but shove it as far away as possible, and give me a stick to keep it away” and “Let’s make null normal and boring so we can manipulate it like anything else”
(“the stick” is AnyVal | AnyRef/NonNull, “making it boring” is ensuring toString/etc don’t blow up in our faces, and stopping it being a special case in the type hierarchy and typer)

And I would expect that everyone also agrees that:

  1. if the value null behaved in the same boring way that () does, it would be good (Proposal: Fixing null.methodOfAny() under explicit-nulls)
  2. if we degrade interop, it would be bad ("MINUS_NULL" and the future of explicit nulls)

And as a result, I think the way to resolve this issue is to prove that we can do the first without doing the second.

It even seems like there is a path we can follow to gradually shed doubts:

  1. Do Proposal: Fixing null.methodOfAny() under explicit-nulls (without Null <: AnyVal): null is less of a footgun (not a footgun anymore ?) and we preserve the ability to exclude it from generics => no possible loss wrt interop
  2. Make Null <: AnyVal and add NonNull, makes null even more normal, and we preserve same capability wrt interop
  3. Remove NonNull once we show that it is not useful anymore, either because it never was, or because we added feature(s) tailored to better suiting what it was useful for

They make sense as steps, but note that 2 and 3 would most likely have to happen in the same release, otherwise we would have too many backward compat issues.

1 Like

Indeed - i was inquiring for a real evidence based reason that a type that excludes null should be needed - that is not based on “null is ugly” - when the proposal is to make null behave more like () in a generic context (i.e. a useless value that should no longer throw NPE unexpectedly.)

however - this is probably more suited to the other thread

You can make the same argument about JavaScript. And in JavaScript, null has type Null which is considered a primitive type.

No. Explicit-nulls should make it so that no NPEs are thrown at all :slight_smile: In the same way that our existing type system ensures that no ClassCastExceptions are thrown at all (as long as one doesn’t perform an incorrect asInstanceOf/nn, as usual).

This change is only proposed in the context of -Yexplicit-nulls, which is experimental. Therefore, breaking changes don’t count.

It seems I need to repeat this every 10th reply: if you don’t provide a concrete use case for a T <: AnyVal bound, it’s not worth discussing what would happen to such code.

Or: don’t add NonNull. Wait and see if it’s actually missing before introducing it → no backward compat lockdown.

It turns out this is not true. There is already no straightforward way to implement NonNull. See "MINUS_NULL" and the future of explicit nulls - #25 by sjrd . And therefore, it can’t be argued that it will worsen Java interop.

1 Like