Kotlin: The Problem with null
arturdryomov.online
arturdryomov.online
This is not correct. The Kotlin compiler could treat every parameter and return value that doesn't have a nullability annotation as an implicitly unwrapped optional (Type!). It could even support all of the popular nullability annotations. There is no requirement to choose just one.
The beauty of IUO is that you can ignore the nullability and you have the same amount of safety you have today: if you touch a null value you get an exception (or abort in Swift's case). The benefit is you can insert a null check in one place which then propagates through the rest of your Kotlin (or Swift) code.
Swift integrates and interoperates with but is not based on the Objective C runtime. Apple also controls all the relevant bits and pieces and they can bridge, compile-to, shim, wrap, etc however they want. They can can pick the design approach and fiddle with all the parts to make them fit. This isn't a luxury Kotlin has.
But the VM is OVERWHELMINGLY designed around the requirements of Java and to be performant for Java. Try compiling a non-OO language to fast bytecode without spending a lot of time considering what the analogous Java code would compile to.
Java bytecode is so close to Java, you can decompile it almost directly to readable Java source. Try that with JRuby or Clojure or Scala and see how close you land.
Apache Groovy 1.x was dynamically typed only, and focused on scripting and glue code for the JVM and syntactic compat with java, so could be said to "overlay Java". Since Groovy 2.0 however, it changed tack with static typing targeted for Android development, not keeping up with syntax changes in Java such as lambdas from Java 8, and tried to compete with Java instead of complementing it. It no longer "overlays Java", and in fact lost out to Kotlin and Scala in its effort to compete with Java. Groovy should have stuck to its knitting instead of changing direction every few years.
https://ceylon-lang.org/blog/2013/04/01/java-null/
I think this is pretty cool. I haven't used it in practice.
I want something in between Kotlin and Scala. I want option as a real type that is treated as a max-single-element collection that can be flattened with the same APIs as other collections. But I want no runtime penalty. People in Rust are so lucky to have zero cost abstractions for these things. I suppose I'll need better compile time support (even more than Scala macros) and whole program optimizations (i.e. cross JAR) to get zero cost optional on the JVM. Things like Scala Native use LLVM and surely Option things are inlined or otherwise optimized out.
Instead you just get a crash if it is null which is worse in my opinion.
> And all of the null checks would have muddied up code and added (admittedly minimal) runtime costs.
This already happens automatically for parameters to a Kotlin function; check out the Kotlin Intrinsics checks.
Only in the same instances you would in Java-to-Java. So the ergonomics aren't improved or reduced. And really, this only helps on returns anyways. Marking every Java param nullable gets you nothing if the implementer didn't handle it well.
> This already happens automatically for parameters to a Kotlin function; check out the Kotlin Intrinsics checks.
Yup, and I don't like it. So even public Kotlin-to-Kotlin calls suffer. Haven't checked in a while, but I would like an option to use annotations only and skip those top-of-method checks.
I've measured the null-checks and other people have too... the runtime performance cost is negligible, so disabling them would get you nothing useful. If they did, Kotlin would have made the compiler flag to disable them (which exists) public... but until someone comes up with an actual reason other than "I feel like it's better" that won't change.
I'm tired of being surprised by the bytecode that Kotlin generates. You could argue javac has some magic too, but not near as much and it's pretty well spelled out whereas you won't find the docs for all of these intrinsics.
It's even worse when things just get dismissed as "I feel like it's better" and then when you provide reasons people will say it's not normal. Languages (and their proponents) should not treat the end user (i.e. the dev) like they are stupid, or at least give them the option to turn things off. The trade-off of hiding this stuff is not worth it...it's more like it's the language people doing the "I feel like it's better".
I don't want the bytecodes added. I have tracing I am doing and I need more predictable instructions. It's not just a performance issue.
Also, you should not assume cost of a JVM implementation of things based on your empirical evidence from a single, popular VM. For example, how does TeaVM translate it?
This isn't correct. For example you have a String that was passed as a param and were shoving it into JSONObject. In Java if it's null nothing bad happens (except shoving a null value into your JSON). Shoving a null into a JSONObject in Kotlin won't crash in the put operation; you'd get the crash as soon as the method is called and it does the Intrinsics check.
Not sure which of these kotlin has if any
1. declaring that a type isn’t null with ! appended.
2. Suppression annotations beatable at the file / class / function / line levels to disable the check
3. Facades / Decorators that can be added by anyone that tell the compiler retrofitted additional typing information on existing libraries (the defintely typed / typescript approach)
0 - https://stackoverflow.com/questions/43129365/javas-math-rint...
Reading up on the format [1] it seems that every value is defined, which includes a ton of possible NaN values [2].
[1] https://en.wikipedia.org/wiki/Double-precision_floating-poin...
[2] If you read the actual IEEE 754 spec, one suggested use for a NaN value is the address of the offending instruction(s); otherwise there is no defined pattern for NaNs in IEEE-754.
> there is no defined pattern for NaNs
?
I was referring to using a NaN value without a zeroed mantissa.
A NaN is defined as the maximum exponent (all ones) and a non-zero mantissa. By "no defined pattern for NaNs" I meant there was no fixed value for a single NaN.
There was an experiment to use it as a replacement for the implementation of `scala.Option` in the dotty compiler code base [2], but it is inconclusive so far; it should be tried directly in the collections library.
I think it's impressive† that you got nesting to work, I'm really curious how you pulled that off for longs and doubles without incurring extra overhead.
† Though, given the magic you worked in getting Union types working in the ScalaJS facades I shouldn't be surprised.
And in that implementation, `None.toString == "None"`, `Some(None).toString == "Some(None)", etc. Although that could be changed.
If you think my unboxed option is not monadic, please provide a counter-example to one of the monad laws.
"Comparing Optionals and Null in Swift, Scala, Ceylon and Kotlin"
http://codemonkeyism.com/comparing-optionals-and-null-in-swi...
Personally I like Monads better than the Kotlin solution. Best is probably the way Swift does it.
fun f(a1: Int?, a2: Int?): Int {
if (a1 == null || a2 == null) {
return 0
}
//now you can do something like
return a1+a2
}In contrast, if it didn't, the code would look like:
fn func1(q: Option<i32>, z: Option<i32>) -> i32 {
if let Some(q_in) = q {
if let Some(z_in) = z {
return q_in + z_in
}
}
return 0
}Note the deeply nested parenthesis.
fn :: Maybe Int -> Maybe Int -> Int
fn q z = fromMaybe 0 $
do
q_in <- q
z_in <- z
return (q_in + z_in) fn (Just q) (Just z) = q + z
fn _ _ = 0 fn x y = case (x, y) of
(Just q, Just z) -> q + z
(_, _) -> 0
but the case becomes implicit in the repetition of the function name at the top level of indentation.At the same level, really - it works in a let or a where as well.
fn = liftM2 (+)You're missing the point entirely.
> the original example gets rid of the Maybe, defaulting to 0.
http://hackage.haskell.org/package/base-4.10.1.0/docs/Data-M...
fromMaybe 0 $
(+) <$> xm <*> ym def func1(q: Option[Int], z: Option[Int]): Int = {
(for {
qval <- q
zval <- z
} yield q + z).getOrElse(0)
} if let (Some(q_in), Some(z_in)) = (q, z) {
q_in + z_in
} else {
0
} case (xm, ym) of
(Just x, Just y) -> x + y
_ -> 0 match (xm, ym) {
(Some(x), Some(y)) => x + y,
_ => 0
} func1(a: i32, z: i32) -> i32 {
return a + z
}
I know it's not 1 to 1, but the idea is there. You would then use a tiny bit of glue code to combine all the stuff you have to get what you want. For example, if you have 2 nullabes as parameters, you use liftM2; if you have 1 nullable, you use liftM; or perhaps you just want reduce a structure, so you reach for foldM. etc. If your monadic code has to constantly figure out what monad it is, you aren't buying yourself much and I could see why you don't find them valuable. And if the function explicitly needs an Option, then it must be important and must be taken into consideration by the caller. I just don't think they should force the caller to consider them where not needed.I wanted to mention I often see similar statements with almost the identical code comparison that you made. I believe it has to do with retraining oneself to think functionally instead of imperatively. I'm curious about your background.
For good measure, here's another example in Haskell:
func1 a b = fromMaybe 0 (liftM2 (+) a b)
And an uglier, but fun, point-free version: func1 = (fromMaybe 0 .) . liftM2 (+)
I believe real-world examples would hold up better because the glue code would only be where needed.Yes! Absolutely. Very important observation. In fact in Haskell Maybe should not be part of the signature if you want to return Nothing whenever one of the arguments is Nothing.
> And an uglier, but fun, point-free version
Yikes, please don't! People might get the wrong idea about how good Haskell is written.
I wrote a little bit about how to work with the Future Monad in "A Little Guide on Using Futures for Web Developers'
http://codemonkeyism.com/a-little-guide-on-using-futures-for...
I would think optional types and everything else being non nullable is the way forward for java too
Maybe in time most of the necessary open source projects will be pure Kotlin ones, but for now I believe annotating Java code with null constraints is certainly a boon for everyone and should be encouraged.
[1]: https://checkerframework.org/
Mostly true under the hood; optional value types are tagged unions (I didn't know this before reading this article and checking for myself), but optional reference types are still, as you'd expect, nullable pointers.[1]
Which is just my way of saying over and over again that tooling can improve that does not require a complete rewrite of your software.
And to be clear, there are still limitations. It can't say if any value you pass into a function that is not part of the current compilation is safe. For somewhat obvious reasons.
But for a large large class of bugs, using advanced tooling can go a long way. As evidenced by the power of the advanced tooling new languages bring into the compilers. :)
It can tell you that, "if this line is reached, and it was called from this path (with evidence on how this could happen), it will dereference a null." It is not saying that "running this program will guarantee give you a null dereference."
The halting problem is more in line with "this program will terminate on any possible inputs", which is more expansive. It is trivial to say that you can prove some programs won't terminate on a particular input. Question is if you can do it for all inputs, no?
But I don't think the Op was claiming it was possible to do this perfectly, just possible to write very useful tools. You will always have either flase positives or false negatives (or I suppose inputs where you just hang).
Turing completeness is the bane of static analysis, but that doesn't make it a fruitless endeavor.
[1] Recent headline 'Phantom Code Paths Found Harmful'
So sure, sometimes Coverity will fail because of the halting problem, and in those cases it can give an appropriate error message. Most of the time, though, it'll work just fine and be a very useful tool.
Same with Rust: it can determine multi threading issues from the surface layer, whereas in other languages in order to detect data races and contention you need some serious tracing analysis as well as doing a lot of runtime profiling.
val foo: T? = null
Observable.just(foo).subscribe()
Here foo is defined as nullable, hence the problem. If you make it val foo: T = null
Observable.just(foo).subscribe()
the kotlin compiler will bark on foo being null and you'll never have a runtime error.I think the motivating example should be described better but maybe I misunderstand it.
Almost. The point about the language itself respecting the Java nullability annotations is apt.
> Here foo is defined as nullable, hence the problem.
How do you know it's "the problem"? The answer is that you don't until it blows up in your face, because this is a Java interop call.
And of course the local could come from an other call into Java which returns a nullable reference, or it could make complete sense for it to be nullable.
> How do you know it's "the problem"? [...] because this is a Java interop call.
If you're making platform calls, you know you have to deal with nulls (or at least, you should, in my opinion). So you declare returns nullable and avoid passing nulls unless that's documented to be okay.
Kotlin's Elvis operator makes this really easy:
val a: String? = null val b: String = a ?: "alternative"
For example, "foo" is a variable at your boundary between the user and the main logic of the system, and the Observable is in the heart of your code and you expect that it should never be passed nulls.
Now, to your point, you could just make sure that you filter the "nullable" foo through a non-nullable "bar." Which is ultimately what you will do.
The article's point is that if the Observable part had been written in Kotlin, this would have likely been the default. Adding a marker to say "nullable" is how you have to do it in Kotlin. In Java, it is the opposite. You have to add a marker saying non-nullable.
At least, that is my reading.
Because Java doesn't have a language-level concept of non-nullability, Kotlin's interpretation of the type signature for `just` must accept nulls (sure enough, non-nullability is enforced at run time inside RxJava). Kotlin trains you to expect nullability to be a compile-time constraint, and has borderline frictionless with Java. Put the two together, and it's very easy to walk into a runtime-error trap.
Whatever.
A lot of it is a more specific cas of sum types vs union types.
E.g.,
Int*** x;
Can be a lot like Maybe Maybe Maybe Int
It's just that in languages where a type like Integer really means “an integer or a null", the nullability doesn't nest, and no one wants to deal with naked pointers.1. you don't have the attending type-safety of knowing that some pointers can't be null
2. the compiler requiring checking for nullity before every pointer access would be horrendously unwieldy
[0] which can then be compiled back to a regular nullable pointer at runtime