6 ms·
The difference between this approach and the approach C# took when they implemented this a few years ago is intereting. With C#, you enable nullability in a pro
by jeroenhd 2y ago
The difference between this approach and the approach C# took when they implemented this a few years ago is intereting. With C#, you enable nullability in a project, and every variable is declared as non-null unless you explicitly mark it as nullable (or use compiler directives to disable nullability for a block of code). With this proposal, all existing variables will be effectively nullable, explicitly nullable, or explicitly non-nullable.
Kotlin, another JVM language like Java, also follows the C# approach, assuming everything is non-null unless explicitly marked as being non-null, except Kotlin doesn't have backwards compatibility to worry about. Kotlin also offers a special `lateinit var` declaration for leaving a non-nullable type unassigned until initialization in another method, throwing a special exception on uninitialized access, which is an interesting workaround for some code patterns that this approach doesn't seem to cover.
I wonder why the choice to offer three options was made. Why not keep the non-annotated variables as nullable and make annotated variables explicitly non-null instead? I can't think of a reason why you would want to declare something as nullable when not declaring anything automatically makes the type nullable anyway.
I think I like C#'s approach better, although this has the benefit of being usable in a legacy code base without having to solve any of the nullability issues. Then again, the C# approach does immediately highlight nullability issues in a code base, whereas this proposal hides them the same way they've always been hidden.
Additionally, I find this strange:
> One method may override another even if the nullness of their parameters and returns do not match.
This feels like a footgun for when a developer overrides/implements some kind of callback and returns null where the overridden method specifies non-null return values.
- foolfoolz 2y agohow does the project level null flag work for libraries? lets say a library was written with no nullability flag selected (legacy jar). i import that jar into my code. in my code i enable all variables as non-null by default
- gwbas1c 2y ago> This feels like a footgun for when a developer overrides/implements some kind of callback and returns null where the overridden method specifies non-null return values. I've caught "null!" in C# code reviews a few times: My response is usually "You can't fix your code by yelling at it." The reality is that C#'s approach also has some nasty footguns that require understanding the nuances of the "required" keyword, and when values are assigned.
- deleted 2y ago[deleted]
- afiori 2y ago> I can't think of a reason why you would want to declare something as nullable when not declaring anything automatically makes the type nullable anyway. It can more clearly show intent. Is is similar to the argument for using const in javascript: you should use const as much as possible so that when you see a let you know that it is going to be mutated at some point.
- dmichulke 2y agoI suppose the reason is backwards compatibility of code, something that Java values a lot more than other languages. Still, I also suppose there's going to be a compiler flag that auto-assumes non-nullability unless specified otherwise.
- jeroenhd 2y agoOn the one hand Java is very strict about backwards compatibility (still reserving keywords like const and goto even though they're not used (anymore)), but on the other hand Java 9's module system broke just about every Java <=8 project I've ever seen. Project Loom also breaks tons of code because of locking and other threaded side effects that weren't even a consideration before, a trade-off between coloured functions and risking old code breaking. It seems to me that Java is very conservative when it comes to the language itself, but actually compiling and running that language seems less of a concern. In this case, the "specify a flag to make all non-annotated variables non-null" approach would only add a single glyph to the language (the ? for denoting nullability), which I would've expected based on the way Java has evolved over time.
- Tainnor 2y agoAnother problem with (especially older versions of) Java is that the language was so restrictive that many of the popular frameworks rely on reflection and/or bytecode manipulation and these things can and do break frequently across JDK or JRE updates.
- deely3 2y ago> Java 9's module system broke just about every Java <=8 project I've ever seen. As far as I understand, some IDE's and built automation tools did not rollout support for module system immediately, yes?
- jeroenhd 2y agoIt took a while for IDEs and tools to catch up, but I still encounter issues with old codebases to this day, using the latest and greatest tools. The problem isn't necessarily that the compiler isn't called right, but rather that a lot of existing library code suddenly didn't work anymore. By compartmentalising code (a welcome improvement!), projects that weren't designed for this compartmentalisation need a lot of "basically turn off Java module restrictions" command line flags to the JVM.
- QVVRP4nYz 2y ago> I can't think of a reason why you would want to declare something as nullable when not declaring anything automatically makes the type nullable anyway. All existing code is "we don't know if it is nullable until someone reviews it", that is different than explicitly allowing nulls. To add to confusion all Optional<> variables should be not-null or using Optional makes no sense at all.
- marcosdumay 2y agoOptional<> is a problem with all languages that add nulability control as an afterthought. If I have v = Optional<A>, if v.hasValue() does it mean v.value is not null? How does it interact with the nullability control? If v? has type Optional<A>, how do I write Optional<Optional<A>>? (And if you think that type doesn't make sense, you haven't really internalized the "parse, don't verify" rule.) If v? doesn't have type Optional<A>, why the fuck both types exist and when should I use each?
- jeroenhd 2y agoOptional<> is such a weird addition to the language. I hope once this link lands, Optional<> will go the way of the dinosaur. I use Optional<> but only to indicate at the API level that something can return null. I hope the chaining ability (?. in many languages, implemented as Optional.map()) will also one day make it to Java.
- usrusr 2y agoI use Optional quite a lot, but only ever as a poor man's x?.let{f(it.y)} via Optional.ofNullable(x).map(it->f(it.y)).orElse(null); Will it at least get taken care of by escape analysis? These days I'd not even trade the simple but effective approach taken by Kotlin for the full glory of Scala's Option (which is so far beyond the Java Optional).
- deleted 2y ago[deleted]
- joncrocks 2y agoIndeed, Optional<> was designed/intended to be used in this way. https://stackoverflow.com/questions/26327957/should-java-8-getters-return-optional-type/26328555#26328555 https://stackoverflow.com/questions/26327957/should-java-8-g...
- aardvark179 2y agoI think this is all a compromise to allow the introduction of this gradually in a code base and across the standard library. You want to start marking these intentions in interfaces and base classes, but you don't want to immediately invalidate all existing implementations.
- jiehong 2y agoAt the end, there is this section: > Providing a mechanism in the language to assert that all types in a certain context are implicitly null-restricted, without requiring the programmer to use explicit ! symbols. Could be nice, even if just as a compiler flag or a module tag.
- ygra 2y agoI'm currently involved at work with migrating a legacy C# code base to nullable reference types. One thing I'd really like is the explicit marker for non-nullable since it makes it explicit which parts of the code have already been looked at and annotated. For time reasons we've opted to just enable the annotations for now except in a few core places. But we still retain the JetBrains NotNullAttribute and CanBeNullAttribute as markers for now to immediately see where we've made a conscious decision (granted, the latter can be removed since nullable has an explicit marker, but the former has a name conflict with C#'s own feature). So in that sense having the three options would be quite desirable. Especially since a few hundred thousand lines of code aren't that quick or easy to migrate. In another internal project we've been doing this in a different way by sprinkling #nullable enable around the code where we touch it and thus gradually improve nullability coverage (and keep in mind that new code must be in a nullable context). This works as well to make it explicit what is annotated already, but it's a much smaller codebase and team.
- Deukhoofd 2y agoWhat we did was to turn on nullability as warnings everywhere. We then went through them project by project, and fixed them. After we were done with a project, we'd turn on WarningsAsErrors for nullable warnings, and know the project didn't need additional fixes any more.
- ygra 2y agoWould have loved to do the same and that's my plan eventually, too. Alas, no time for fixing right now. And as I've already noticed when migrating from JS to TS or non-strict TS to strict TS, there's a whole lot of issues that require thought and investigation whether everything is fine from a nullable perspective. I have found quite a few issues, even outright bugs, but when there are more pressing issues than a few lingering NullReferenceExceptions that no customer has stumbled over in the past decade, then those get prioritized. At least new code is forced to deal with those warnings for now, which is much easier than fixing legacy code.
- BoardsOfCanada 2y agoMy experience with kotlin and c# is that in kotlin it just works, even with java interop, while in c# nullable value types seem like a far leakier abstraction.
- the_gipsy 2y agoThis way Java can claim that it has this feature when comparing to competing languages, without interfering with all the legacy code. It doesn't matter if it's not useful, some manager can just run down a checklist and pick safe ol' Java.
- pjmlp 2y agoAs someone that works across JVM, CLR and Web ecoystems, dealing with C# approach is still a mess, unless one is lucky enough to only work in greefield projects. It is made worse, by being a C# 8 feature and not usable in libraries that need to stay compatible with .NET Framework (yep still plenty of stuff going on there).
- ygra 2y agoAFAIK you can still use it for older frameworks. The compiler embeds the attributes into the assembly when they're known to not be part of the runtime library [−3.7]. You can do the same with the various conditional nullability attributes. [−3.7]: https://sharplab.io/#v2:EYLgHgbALAPgAgZgARwExIMJIN4FgBQSRKycUSAsgBQD2wAVgKYDGALgPxI0CUOBxSAL4FBQA=== https://sharplab.io/#v2:EYLgHgbALAPgAgZgARwExIMJIN4FgBQSRKyc...
- jacobparker 2y agoThe one nullable gotcha with doing this is that the .NET standard library doesn't have nullable annotations in older frameworks. One approach to adding nullable annotations to a large code-base is to go "bottom-up" on DLL at a time. The annotations are somewhat viral; if you were to go top-down then as you get to more of the "shared" DLLs and add annotations you'll find yourself having to go back and revisit the DLLs you already migrated. In this light, the .NET standard library situation is problematic. Imagine implementing IEnumerable<T> by wrapping another IEnumerable<T>; in .NET framework there are no nullable annotations, so you can get into a situation where you don't add a nullable annotation where you will eventually need one upgrading to newer .NET. This can bubble up throughout your code base, making that eventual .NET upgrade more challenging. I'm not saying its not worth it to do this in .NET framework, but you can very easily add extra work to the inevitable(?) update to .NET 8+. When you get there you could of course temporarily regress and turn nullable stuff back to warnings until you work through that surprise backlog, but nullable is really nice so you might be strongly inclined to not do that... not a huge problem, just something to be aware of!
- ygra 2y ago
- layer8 2y agoI agree that it is strange. What I would have expected is (1) that on the source-code level T? means nullable (the existing semantics) and T! means non-nullable, and that the default for plain T in newly compiled source code can be specified using a pragma-like declaration per source file, per package-info file, or globally by a compiler switch, and (2) that a subsequent LTS Java release would switch the global default. This would allow easy switching of projects (you can insert/update the pragma in an automated fashion), and also allow keeping the old default when needed. In addition, the existing JSR-305 @Nonnull annotation could have been leveraged as being the way how nonnullable type occurrences are indicated in class files, providing two-way compatibility for old JDKs.
- twic 2y agoSounds pretty good. But i think the mode selector has to be in the source code where a reader can see it, which rules out a compiler switch. The current proposal doesn't exclude this possibility in the future, does it. Maybe one day you will be able to declare a class public class! MyClass { or have a package-info file which says package! com.initech.accounting;
- layer8 2y agoThe compiler switch would be for emulating (or for disabling) the global switchover that we want to take place at some point. We currently have the wrong default, so to speak, and we’d want to change that at some point. Then it is useful to already be able to do that in earlier releases, and also to be able to revert to the old default in later releases. Otherwise, I agree that the indication should be in the source, at least during the transitional years. In practice, looking for the presence or absence for T? vs. T! will be a good heuristic for whether the code assumes the new or the old semantics. The compiler could also warn or even error out when the code is obviously being compiled with the wrong semantics, that is, if ? or ! are present but all redundant. Regarding the class syntax, you can have multiple (package-private) classes in the same file, so I wouldn’t do that. A simple solution would be to have an optional ! or ? at the very start of the source file (ignoring comments and whitespace), which means a regex could handle it (useful for external tooling), and it would also work in the absence of a package declaration (i.e. in the default package). Another reason to not put it on the package declaration is that it isn’t meant to apply to the whole package.
- unscaled 2y agoKotlin does have types with unspecified nullability, although you cannot define it directly. Kotlin calls them "platform types"[1], and they're annotated with an exclamation mark (e.g. "String!" for a possibly-nullable string) in errors and diagnostics. Both the term "platform types" and the exclamation mark symbol are confusing, but other than that Kotlin's approach works quite well. Since Kotlin has nullability from the very beginning, the programmer can't specify platform types directly, but Kotlin still needs to support them to maintain compatibility with underlying platforms that support unspecified nullability, such as the JVM and JavaScript. With this approach, the defaults are still sane[2] (always non-nullable by default — everybody who thinks otherwise has learned nothing from Tony Hoare[2]), but the backward compatibility is maintained. But Kotlin got it easy, Java and C# also need to maintain compatibility with existing source code. Neither Java's approach or C#'s approach seems ideal. The C# approach drastically changes the behavior of the code based on a compiler flag, while the Java approach makes the default into the worst possible choice. I still lean towards the C# solution, because I think making "maybe-nullable" the easiest choice, means most programmers will choose it by default. Especially in an enterprise-friendly language like Java. Linters and compiler warnings would help in the long-term, but I think it will be many years before most of the Java code will be properly annotated for nullability. C# users will see more pain in the short term, but they will probably get to the promised land of clear nullability much faster. [1] https://kotlinlang.org/docs/java-interop.html#null-safety-and-platform-types https://kotlinlang.org/docs/java-interop.html#null-safety-an... [2] https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/ https://www.infoq.com/presentations/Null-References-The-Bill...
- simon_void 2y agoyou failed to mention that Kotlin has platform types only if it's interoperates with Java. Platform types are for objects that come over from Java code (and therefore have unspecified nullability (assuming the Java code doesn't use nullability annotations)). Admittedly you provided a link that explains this, but how many people will follow the link? And yes, this detail feels kind of important. And yes, future Java (and Kotlin) will only have their version of platform types to interop with old Java code. As a long-time Kotlin user I can only say: Nullability is nice! Once you know it, you won't want to code without it.
- UncleMeat 2y agoIts a footgun, but one chosen for practicality. There have been a number of other nullability annotation frameworks for java in the past that are more complete and correct, but also more difficult to use and understand for a typical developer. A classic example is "what about nullness of fields defined in constructors?" After all, there is a real moment in program execution where those fields store nulls. So people try to add things to represent "nullable, but once non-null it remains non-null forever" or whatever and then people don't practically use the system because it is too much conceptual complexity.
- lolinder 2y agoAnother practical answer is to just accept that an edge case like that is one we won't completely solve for. Kotlin will yell at you if you don't initialize everything in the constructor and will yell at you if you try to use a variable in the constructor before it's declared, but if you call out to another function in the constructor you're on your own and might end up with nulls. That's not complicated to understand, but it eliminates most types of not-yet-initialized nulls in the language. Also, isn't how you handle not-null types in constructors totally orthogonal to which syntax is used for not-null and whether there's a third I-dunno-if-null type? And is a three-way nullability system really simpler to understand?
- eru 2y ago> I can't think of a reason why you would want to declare something as nullable when not declaring anything automatically makes the type nullable anyway. To show the reader of your code that you explicitly want this nullable, instead of just being too lazy to type? You can then use a linter to forbid the implicit behaviour in your code.