6 ms·
Nullness markers to enable flattening
- topspin 4y agoI don't care how they get there, as long as they get there. In this case they're realizing that NULL makes optimization difficult and are swerving into solving this 27 year old instance of the "billion-dollar mistake." NPEs shouldn't exist in work-a-day code.
- sam_lowry_ 4y agoWhat syntactic sugar they offer is not clear from the mail. There are already lots of techniques, from annotations to Optional.ofNullable but none matches the simplicity of != null. They can also go the route of `effectively final` and make the compiler deduce nullness instead of marking it explicitly.
- yarg 4y agoThere's not really a nice way of implementing the syntax without changing the language. Although perhaps they could resolve this with magic imports? import java.lang.magic.optional; public class Foo{ public static void main(String...args){ Foo? f = ...; if(f){ f.doThings(); } f.doThings(); //compile time error } } The idea being that importing the magic class enables the cleaner syntax.
- usrusr 4y agoI doubt that it's possible to open the door for "language version by imports" without turning that decision into a forever-floodgate. Note that I am not entirely sure that it would be a bad decision (I do lean in that direction however), but it would definitely be a severe one. It would be like a variant of inviting Dracula across the doorposts, only that this one may or may not actually be a cool guy and not a vampire, but in any case he will sure bring all his friends, and their friends and so on forever.
- yarg 4y agoWith great power comes great responsibility.
- kaba0 4y agoDiscussions on the mailing list usually tries to avoid the exact syntax for as long as possible to avoid bike-shedding. But according to this mail there would be a default nullability (hopefully not-nullable as the default, but it may depend on whether it is a value class or not), and ! and ? as modifiers (and * for type variables). So `Point p = null` won’t type check, you would have to mark it as `Point?` where Point is a value (primitive) class. There were also points that it should work with the usual type inference, e.g. `final var p = methodThatReturnsNullablePoint()` would be inferred to `Point?`.
- aardvark179 4y agoReference types probably can’t be made non-billable by default as that would break too much. But String! myString = “Definitely not null”; doesn’t look too bad. Edit: it’s worth saying that a lot of careful thought around compatibility goes into features like this. That means the core libraries should be able to transition to non-billable types where this is already a run time requirement, and existing compiled jars must continue to work.
- vips7L 4y agoI don’t think anyone would really use String! everywhere.
- aardvark179 4y agoI think you’ll see it creep into parameter types in the core JDK and it will work its way out from there.
- kaba0 4y agoI think it can work in a binary-additive way — the class file gets a new attribute for nullness information, while missing it (older class files) default to “unspecified”. That can be safely consumed by new code, which will get the updated syntax with a new option (specified on a module/package/class basis) where ‘T’ means ‘T!’ and that should be a proper subset of previously compiling code, so I don’t think anything would break that way. Similarly to how generics and unchecked warnings were rolled out.
- vbezhenar 4y agoI think the general idea is pretty clear. `MyClass! myvar` is not supposed to accept nulls. Assigning null to `myvar` would throw exception, comparing `myvar` with null is pointless and probably should emit a warning. Or maybe even error. `MyClass? myvar` is supposed to accept nulls. Dereferencing this value without checking for null is either warning or error. `MyClass myvar` is supposed to be unspecified with regards to nulls. So basically current code. You can do whatever you want but you should refactor it to `MyClass!` or `MyClass?` some day.
- cogman10 4y agoIt'll allow the JVM to more aggressively optimize the code (the main benefit). Annotations are, unfortunately, all over the board. There's a dozen different `NotNull` annotations out there. Having it in the language will make it the last solution to this problem. You can't do deduction at compile time because you'll end up with different method signatures if someone calls a method with a null vs not. (not to mention the problem with someone calling these methods with reflection) You can do deduction at runtime (and they likely will), but then you have to deal with the problem of unwinding an optimization when the hypothesis is proven wrong. That can be really costly when the optimization is dealing with memory layout. (Imagine an array of value types where you deduced they should be non-null, yet some unexpected path inserts a null. In that case you have to reallocate the entire array and it's elements in order to handle that). It's more than just syntactic sugar.
- yarg 4y agoI've alway felt "billion dollar mistake" is unreasonably hyperbolic. In retrospect it would be very nice to have a language level ability to define something as optional, and require existence checking for such values; But it wasn't so obvious at the time.
- highwind 4y agoI believe ML family of languages do not have null. And it was developed well before Java. So I don't think timing was the issue.
- pjmlp 4y agoEiffel also didn't, and additionally it supports value types (expanded classes).
- pkulak 4y agoCan you expand on that a bit? Why would having it built in to the language (Kotlin-style, for example) be better than not having the concept of null in the language at all and using something like the Rust Option? Personally, I like the Rust approach. It seems a bit more flexible and can evolve easier over time.
- cogman10 4y agoI think the main issue with the Rust option is they ended up with language syntactic sugar anyways. So in some ways, it's worse than the kotlin-style because reading `let baz = foo?.bar` could have all sorts of weird type implications. What is baz? Is it Option<Bar>? Is it Result<Bar>? Is it some user defined enum<Bar>? Who knows! You have to find that by looking up the foo definition. For kotlin, the answer is simple. "baz is a nullable Bar" Rust did this because interacting with the enum directly was cumbersome. The concept of "no value" is so integral to day to day programming that elevating it into the type system with "nullable types" makes more sense to me vs using generics trickery. Even if it's slightly less "pure".
- 4y ago
- deleted 4y ago[deleted]
- saagarjha 4y ago> In this case they're realizing that NULL makes optimization difficult How so? Null checks are mostly “free” on the JVM, they trap on the zero page and get converted to an exception via a signal handler.
- Someone 4y agoIt’s not only the checks, but allotted storage. An Integer that’s guaranteed not to be null can be stored in 4 bytes, a “Point” class with two “int” fields that is guaranteed to be not null can be loaded in a 64-bit register, etc. That makes it easier for the compiler to generate good code.
- pjmlp 4y agoEiffel was already doing this with expanded classes and non void references, but yeah, who would have thought such details are relevant. :)
- marginalia_nu 4y agoDo NPEs exist in work-a-day code? I remember working on a codebase where everything had null check pyramids like if (foo != null) { if (foo.getBar() != null) { Bar bar = foo.getBar(); if (bar.getBaz() != null) { for (Quux q : bar.getBaz()) { if (q.getFoobar() != null) { ... } } } } but that was a long time a go in a code base that operated by passing humongous mega-objects with hundreds of fields (running into the limit of how many parameters could be in the constructor was a constant headache), it solved a lot of its problems by printing a stacktrace and shrugging, and objects would be half-constructed and methods would return null in all sorts of scenarios. In comparison, the entire codebase for marginalia search has about 65 nullchecks in total. A decent chunk of them are in dealing with older APIs that sometimes idiomatically use nulls to communicate things. java.util.Map, BufferedReader and the Servlet api in particular, the rest in dealing with the inherently noisy nature of crawl data. So while there are a few examples where there are nulls in the data, but they are almost always contained to the module that produced them and don't cross interface boundaries. It's very far removed from some pinnacle of code quality, but just gets a few engineering principles right that that other code base didn't.
- dangets 4y agoMaybe this is from my epoch of education, but with early Java null checking everything was promoted as a good practice. Even today, depending on what your teacher said was correct, your coding experiences (e.g. if you came from C++, if you read 'Effective Java'), you might still null check everything. Personally, I lean towards the "use nulls sparingly and be very explicit where null values are possible", use immutable objects etc. This reduces the unnecessary null-check noise and makes things more robust. In java, I even prefer to add @Nullable to fields to try to switch the default mindset to "assume non-null unless @Nullable annotation exists" - though another developer made a note on a code review that "that is not how Java is designed and @Nullable is just noise". Having nullability as part of the type makes everything more explicit, so I'm a huge fan. Beyond just the perf and safety benefits, it's a win for domain modeling.
- pjmlp 4y agoML solved it in the 70's, Eiffel in the mid-90's, however just like NPE there were quite a few things that Java didn't adopt, and here we are.
- mabbo 4y agoIf I'm understanding this correctly, is the author saying that a future version of Java may let me use `String! foo` and the compiler will make sure this variable cannot be null? That would be very helpful indeed.
- zwkrt 4y agoExplicit nullability is one of my favorite features of kotlin. Kotlin basically treats nulls like a checked exception: deal with a null value immediately or incur the mental overhead of having your data treated like a 2nd-class citizen in all the code it touches.
- madeofpalk 4y agoIt baffles me that golang does not have this, which is otherwise a relatively modern language.
- adastra22 4y agoThere's a lot of stuff golang doesn't have :(
- sam_lowry_ 4y ago...to go faster.
- talideon 4y agoIf Go were a modern language, it would've had algebraic data types and pattern matching since the beginning, and its handling of errors and enumerations wouldn't be quite so embarrassingly bad.
- 3836293648 4y agoGo is not a modern language. It's an 80s language that happened to be made in the 2010s
- deleted 4y ago[deleted]
- revskill 4y agoIt's in correct direction to make nullness more explicit through the type system.
- jstimpfle 4y agoI remember reading about this project first in 2014-ish, how long has this been going? Seems like this is a ton of work, only to be able to encode constraints to maintain correctness and performance like you can in a memory unsafe language. The latter has always allowed to "postulate" dropping identity by value copy and to encode 1 to 1 relationships by direct embedding. I think I've been corrected in the past, where someone pointed out that managed languages don't have to be like that? What are existing approaches that allow for better control than traditional Java offers while retaining a convenient syntax? Concerning the languages I know -- memory managed, reference only models always come with the possibility of indirection at every turn due to not having the distinction between dereference and member access (not in the syntax and not in the semantics). It's very hard to infer from the code if a dot operation is an injection, mathematically speaking. I believe that is the reason why I've always felt "safer" and more precise in memory unsafe languages. I also think that some of the newer languages are misguided to hide the distinction by using dot syntax for both.
- kaba0 4y ago> come with the possibility of indirection at every turn due to not having the distinction between dereference and member access Most managed languages don’t expose the location of an object, only its semantics. With optimizations (escape analysis), an object can be allocated on the stack and thus only effectively make member accesses. This optimization can be done much more deterministically with value types — if an object looses identity then the runtime/compile time doesn’t have to reason about whether it leaves the function/loop, it is free to copy it in/out of functions as often as needed. As for preferences, it makes sense in low-level languages, but imo hiding it is the correct choice on a higher level — this is a core benefit of GCs, e.g. your public APIs will be much more maintainable/won’t need refactors because they don’t expose low-level implementation details (where should that argument be stored), only its semantics.
- pjmlp 4y agoNote that using dot syntax across records and pointers is as old as Mesa and Ada.
- titzer 4y agoI worked on JVMs (in a mostly research capacity) for a good number of years, but I soon gave up on the idea that classes and instances of classes (thus, heap-allocated objects) are the only thing the VM and language should be designed around. But there is a lot of sunk cost in the engineering of the JVM and there seems to be a lot of inertia against adding new primitives like tuples or unboxed types. That limits the source language design somewhat and definitely brings down the performance ceiling, raising the complexity of the engine, ironically (because it has to work extra hard to maintain the fiction of objects with identity). Well anyway, I diverged from that significantly now; Virgil has value types, tuples, ADTs, and closures and happily unboxes them all.
- vips7L 4y agoImo named tuples via records are better than tuples provided at the primitive level. They provide a ton more context for the next reader about what this thing actually is.
- yarg 4y agoIt'd be nice if you could inline the Tuple description into the method signature. There's a bit of unnecessary overhead in having to externally declare and import the return type. public [K key, V value] getElement(int index); This really comes down to the dynamic vs typed argument, and to me the answer is "minimise the boiler-plate, maximise the guarantees.".
- haimez 4y agoI think declaration vs use Sue distinction is much more helpful to reason about in this context, and named tuples are a classic example of “if you didn’t declare these tuples to be compatible at definition time, it’s probably a bug to let you pass them around as if they were”
- yarg 4y agoCodesmell sure - and a case for refactoring, but I wouldn't go as far as to say bug. But I wouldn't expect that to be the common use case - I'd generally expect that the caller would just be implicitly importing and using the type effectively declared in the method header.
- lmm 4y agoI despaired of the Java process' ability to fix this kind of issue when I saw what a hash they made of the Optional implementation (not allowing Optional to contain null undermines most of its advantages, and destroys all incentive for migrating to Optional if your codebase isn't going to reach 100% null-free in the forseeable future; not to mention the (explicitly stated!) desire to "not be a monad" i.e. to have wilfully idiosyncronatic behaviour). Trying to make this a not-type is doomed to failure. It's a property of expressions, it has rules about how it flows through expressions; that means it's a type, or it's an ad-hoc informally specified implementation of half a type. It's recapitulating the mistake of checked exceptions all over again.
- deleted 4y ago[deleted]
- kaba0 4y agoCould you expand on the “not a monad” part? Java’s type system with generics is surprisingly expressive, but it can’t have higher kinded types, so a real Monad interface is not possible as far as I know. But it does have methods to use it as you would use a Maybe in Haskell, so I don’t find its occasional use problematics at all. As for checked exceptions, I disagree that is a mistake. They correspond one-to-one with Result types, but are just better integrated in the platform (has proper stack traces, bubble up as default, and auto-unwrap). Java’s implementation has much to be desired (it didn’t have sum types back then), but checked exceptions themselves should get a reevaluation.
- lmm 4y ago> Could you expand on the “not a monad” part? Java’s type system with generics is surprisingly expressive, but it can’t have higher kinded types, so a real Monad interface is not possible as far as I know. But it does have methods to use it as you would use a Maybe in Haskell People in the design mailing list discussion argued that those methods shouldn't be added because they didn't want Optional to be a monad. > They correspond one-to-one with Result types They don't, because throwing them is not a value (there is no way to have a variable that you can substitute for "throw e" and get the same behaviour), and because they're not properly integrated with the language's type system. Throws-ness is a separate parallel universe that behaves almost but not quite the same as expression type. Which is exactly what this is proposing to do with nullness.
- zvrba 4y agoEach time I read one of posts about JVM, I'm impressed by how they manage to deconstruct a "high-level" concept such as value type or reference type to more basic ingredients. Pinnacle of bottom-up design.