4 ms·
We've been using `@Nullable` as Meta does in our code base for many years now (everything else is assumed to be non-null). Initially, we had CheckerFramework ac
by brabel 4y ago
We've been using `@Nullable` as Meta does in our code base for many years now (everything else is assumed to be non-null). Initially, we had CheckerFramework actually doing the checks at compile time... but eventually had to remove it because it's unstable, slow, has not kept up with Java evolution (we're on JDK 17, I think Checker still barely works on JDK 11) and also, because our IDE can be configured to flag errors on `probable nullability issue`! That was really a boon for us... it costs nothing at compile time and it's pretty easy to not mess up as the IDE keeps us in check whenever we make any change.
At the same time, we have adopted Kotlin as well. Interestingly, most of our developers prefer to stay on Java... not because they love Java, but because of familiarity and that they seem to think it's "good enough".
Anyway, we haven't had a large rate of NPEs for a very long time and when one actually happens in the wild we're extremely surprised!
That said: looking forward to the specification[1] that's being cooked up as Java absolutely should have had this a long time ago (as mentioned in the post).
[1] https://jspecify.dev/docs/start-here https://jspecify.dev/docs/start-here
- clumsysmurf 4y ago> At the same time, we have adopted Kotlin as well. Interestingly, most of our developers prefer to stay on Java... not because they love Java, but because of familiarity and that they seem to think it's "good enough". I have been using Kotlin on Android for a few years now, and really enjoy the extra expressiveness and conciseness that the language offers ... but have come to the conclusion its much easier to write unreadable kotlin code than java code. One of the things I think makes Kotlin hard to read is scoping functions (apply/also/let/run/with). Based on their names alone, the newcomer can't tell what they do (I had to consult a table for weeks on whether they returned the original object, last expression, and whether they passed it, this, etc). I do love these when used at the right place, right time, and "just enough" but I keep coming into contact with code that nests these 4-5 levels deep: aaa?.run { bbb?.run run2@ { ccc?.run { ddd?.run { this@run2.foo() // whats in scope here urgh } } } } Other pathologies I see are making any method (foo), that operates on X, as an extension method X?.foo(). Then the namespace for X gets polluted with all these things. One of the great things about Kotlin's nullability in the type system is the operators like ?. and ?: ... but often see code like this: aaa?.bbb?.ccc?.ddd() Is it safe, yes, yes it is. It won't crash, but what happens if one of these is null, and should not be? There is nothing in Kotlin forcing people to write this way, but find Java's explicit checking of null made people think more of the "negative case" (throw an exception? etc) Like anything else, I guess we could use linting rules, it comes to having good taste ... which is not in abundance. What worries me the most though, is that in the beginning, Kotlin was mostly additive on Java. I read, in biological systems, adding is easier that changing or removing. It seemed like Effective Java + Typed Groovy. Now as Java has "last mover" advantage, and adopts the good parts of Kotlin, I am worried its not just additive, they need interop between different implementations (records vs data classes).
- titzer 4y agoWhoa, interesting. I didn't know Kotlin had all those constructs. In Virgil, a method on an object (or ADT) can declare its return type as "this". Then the method implicitly returns the receiver object. That trick is very useful to allow a chain of calls such as object.foo().bar().baz(). I find it readable and easy to explain: https://github.com/titzer/virgil/blob/master/doc/tutorial/ReturnThis.md https://github.com/titzer/virgil/blob/master/doc/tutorial/Re...
- Larrikin 4y agoIn your first example, thats poorly written code that should never make it past code review. I'm also having a hard time thinking of a case where run would even be useful in that situation that wouldn't also fix your second example. If you need all aaa through ddd to be non-null you return early or you check ahead of time aaa ?: return bbb ?: return ccc ?: return ddd ?: return or if (aaa != null && [the rest] != null) foo(aaa, bbb, ccc, ddd) Since if foo needs them all to be not null they should be passed in aaa?.bbb?.let{ bbb -> //definitely should not be null in this case bbb.ccc?.ddd() } ?: return // or throw an exception
- usrusr 4y agoAnd val aaa = aaa ?: return val bbb = bbb ?: return val ccc = ccc ?: return val ddd = ddd ?: return if those three-letter friends happen to be "getter vals" that might change behind your back (the compiler is perfectly aware of the difference). That's the one gripe I have with kotlin, those "getter vals" that aren't even remotely related to immutability. Likely a consequence of building closely on top of java, there's simply no way an interface could promise to never return two different references from a call on the same object without that promise being a lie. And as usual, kotlin provides gentle damage mitigation on syntax level: "val x = x", as in the four lines above. I would have preferred something even more consise like "val=x" though, where the name overlay is promoted to explicit intention by not typing it twice, as in "call that getter and nail its return to the getter's name for the remainder of our scope". People tend to be reluctant with that name hiding, unsure of it's good style or bad, if there was an explicit shortcut it would be blessed as The Kotlin Way (and, as you said, it's so much better than getting pulled into that let/also nesting abyss that feels clever while writing but turns on you before you are even done)
- sixbrx 4y agoChecker framework works fine on JDK 17 since not long after the release