PROPOSAL: Null safe type system

Alessandro Autiero alautiero at gmail.com
Sun May 15 17:59:36 UTC 2022


Null safe type system

Hello, this is my first time writing a proposal for OpenJDK, but I hope
that it will meet the quality standards.
I've looked through the discussions in the OpenJDK mailing lists, but I
couldn't find any regarding this issue, so I thought I'd propose something
on my own.
I discussed this proposal previously on the java subreddit here
<https://www.reddit.com/r/java/comments/tiw76i/backwards_compatible_null_safe_nonnull_by_default/>to
collect ideas to make this proposal as comprehensive as possible.
I apologize in advance for any mistakes.

**OVERVIEW**

The Java Type System is made up of two different types: primitives and
objects.
While the first cannot be assigned to the special type null, the latter can.
If an expression, for example, a method invocation, takes as an argument a
null reference a NullPointerException is thrown.
This design choice introduces the need to check for null values to make
sure that this doesn't happen.
The most common values that are checked are method parameters, especially
if the method is exposed publicly, and ones returned by method invocations.
To make the latter easier, in Java 8 the java.util.Optional wrapper class
was introduced.
Being a class, Optional is not suitable as an equivalent to, for example,
Swift's Optional which is used often as a method parameter.
Despite this obvious design choice, some very popular libraries in the Java
Ecosystem, most notably Selenium in my experience, have chosen to use
Optional in the context just described.
This indicates firmly that there is a need for better handling of null in
Java's type system.
As a matter of fact, annotations have become a sort of fix for this
issue(see the Checker Framework for example), though, in my opinion, this
issue should be resolved in the type system itself.
What I'd like to propose is to improve Java's type system by leveraging the
Optional wrapper, the module system introduced in Java 9(Jigsaw) and the
recent developments over at Project Valhalla to better handle the special
type null.
Considering that our neighbour in the JVM universe, Kotlin, has been
designed with a type system that takes into account exactly this issue, I
think that it's appropriate to discuss JetBrain's approach.
Kotlin's type system, differently from Java's, is made up only of objects.
While non-null scalar types, such as Int, use JVM primitive types as an
implementation detail, they are not part of the language.
Object types can be nullable(the name of the type is followed by a question
mark), non-null(the name of the type) or platform types(the name of the
type is followed by an exclamation mark) to provide a compatibility layer
with Java's type system.
In the latter case, the programmer should explicitly declare the type as
nullable or non-null(it's not technically needed as the compiler will issue
a warning and not an error, but it's advised), while in the others can be
inferred.
In practice, this means that, for example, the type Int and Int? are two
completely different types for the compiler.
While this approach can be appropriate when designing a new language, like
in Kotlin's case, it certainly isn't feasible in Java's case considering
its twenty years of legacy.
Now let's look at Swift's approach instead. Swift's type system allows for
nullable and non-null object values and follows the same naming conventions
as Kotlin, except platform types aren't a thing here.
Furthermore, nullable types aren't compiler magic in this case: they are an
alias for the Optional enum.
For example, the type Int? and Optional<Int> are the same type.
This also means that declaring, for example, a variable with type Int?? is
legal, as this simply translates to Optional<Optional<Int>>.
This approach is better suited for Java as we already have a very similar
class that we can leverage: java.util.Optional.
Implementing the same concept in Java wouldn't have been previously
possible because the Optional wrapper, being a class, is too slow.
Thanks to Project Valhalla, though, the Optional wrapper can be transformed
in a primitive class.
Non-null values assigned to Optional types should be autoboxed by using
Optional<T>#of(T), nullable values should be autoboxed by using
Optional<T>#ofNullable(T) while the null literal should be an alias of
Optional<T>#empty().
This can either be done with some compiler magic, like we currently do with
primitive wrappers, or by introducing a full-fledged feature to support
boxing and unboxing for classes, but this conversation is not relevant here
I think.
This means that the performance hit should no longer be a concern.
NOTE: An instance of a primitive class doesn't allow for null initializers,
though, considering the previously described boxing and unboxing mechanism
this should be syntactically legal. If this approach introduced ambiguity,
though, a better approach would be to use a value class instead.

**CHALLENGES**

As previously mentioned, Java's legacy makes implementing such a feature
quite hard when considering backwards compatibility.
The first step is obviously to make Optional into a value class and this is
possible without breaking anything, as far as I know.
Furthermore, it's also necessary to make sure that a developer using a Java
version with this new hypothetical type system can, for example, use a
library built using a version of Java that doesn't.
This can be technically done by treating older types as equivalent to
platform types and allowing javac to place null checks(NULLCHK operator)
when needed.
While platform types as a concept aren't strictly necessary, I think that
having older types be nullable by default is not the right approach as if
the programmer wants to declare the variable as non-null requires implicit
conversion(from ExampleType? to ExampleType), which goes against the
purpose of this type system, or a manual cast which can be perceived as
very verbose and redundant.
This challenge was already faced by Google when they transitioned to Dart
2.12 which introduced the concept of nullable and non-null types.
Finally, the only challenge would be to make migrating a project from an
older Java version as painless as possible.
For this new type system to work effectively, types should be non-null by
default.
Though, Java's current type system has object types nullable by default.
This means that if a developer were to transition to this new hypothetical
version, everything in the codebase would break.
To face this issue, I think that introducing a flag, for example,
sound(sound null safety), in the module-info, just like open, is the best
approach.
The default flag would be, for example, unsound, so that backwards
compatibility is preserved even if a module-info is already present.
Some may argue though that migrating a whole module to sound null safety is
unlikely in major, especially enterprise, applications all at once.
This is the reason why we might consider having this flag also on a package
level by using the package info.
An idea would be to have the flag in front of the package declaration,
though there are currently no flags available for packages in package info,
having different type systems in a single module is not optimal for a
developer working on a codebase and there should also be a hierarchical
resolution of flags(for example a package is unsound but its parent module
is sound, what do we do?).
Apart from these implementation details, this approach ensures backwards
compatibility and an easy transition for developers to newer versions.

**COMPLEMENTARY OPERATORS**

To support such a type system, at least the null-safe de-referencing
operator(?.) would need to be introduced.
This should not break any existing application because the question mark is
not a legal character for variables' names, so no ambiguities can occur.
Other useful operators would be the Elvis operator(?:) which is a binary
operator that returns the left operand if it's not null otherwise the right
one and the double bang operator which is a unary operator that throws a
NullPointerException if its operand is null.
These aren't strictly needed, but they certainly improve the developer
experience.

**COMPLEMENTARY KEYWORDS**

Introducing an equivalent to the var keyword, introduced in Java 10, for
nullable types could be appropriate.
Let's say that the initializer of a variable is a literal, but the
developer later wants to assign a new value to this variable: how should
the compiler know if the type to infer is ExampleType or ExampleType?.
The var? keyword would only allow for nullable types, while the var keyword
for both nullable and non-nullable.
This, though, should probably be discussed.


**EXAMPLES**

Module Flags:

module com.example.project { // The module has unsound null safety

}

sound module com.example.project { // The module has sound null safety

}

unsound module com.example.project { // Unsound is redundant, the module
has unsound null safety

}

Example interaction between legacy code and modern code:

public void someMethod(){
   String? someValue = OldAPI.getSomeValue(); // The return type of
getSomeValue is specified by the programmer to be nullable, no checks needed
   String someOtherValue = OldAPI.getSomeOtherValue(); // The return type
of getSomeValue is specified by the programmer to be non-null, the compiler
should include a NULLCHK
   var inferredValue = OldAPI.getSomeValue(); // The return type of
getSomeValue is inferred to be nullable by var considering it's a platform
type
}

**CONCLUSION**

Discussing the technical changes needed for javac and for the JLS is
necessary, though this proposal will probably need a lot of discussion and
opinions before we get to this stage in my opinion.
The effort would probably be considerable though in the case of the
compiler.


More information about the amber-dev mailing list