Hacker News new | ask | show | jobs
by lmm 3380 days ago
Nullable types are worse than Option in every way that matters (yes they save a few bytes of memory occasionally, but who the hell cares, if you care about shaving 8 bytes off an object you won't be using the JVM in the first place. It's certainly not worth the cost of working with a weird, second-class type that you can't abstract over like other types). Why do you want an extra way of representing the same thing with more special cases?

> Kotlin's approach is superior in that it requires developers to deal with nullability

But it doesn't force them to deal with nullability when interacting with Java which is the only case where it matters.

Scala calling Scala: you never use null, you never hit a problem with null.

Scala calling Java: you have to manually check return values for older libraries but at least passing in null can't happen.

Kotlin calling Kotlin: you use null a lot, you never hit a problem with null.

Kotlin calling Java: you have to manually check return values for older libraries and won't notice where you're passing in null because that's a normal, safe thing to do when calling Kotlin.

2 comments

I think there's some misunderstanding about what goes on with an Option<T> type in the compiler. In a sane implementation of Option<T>, there's no space premium for using it over T. The compiler compiles Some<T> to a pointer to T, and it compiles None to null. Swift, Rust, and Haskell all do this; I don't know Scala that well, but I'd imagine it would too unless there's some edge case in Scala<->Java interop.

Kotlin's nullable types and Option are exactly the same thing, except with some implicit conversions and smart casts available (marked by the IDE). Using Kotlin & Rust syntax, I make the following mental equivalencies:

  T? <==> Option<T>
  maybeNull!! <==> maybeNull.unwrap()
  maybeNull ?: default <==> maybeNull.unwrap_or(default)
  maybeNull == null <==> maybeNull.isNone()
  maybeNull != null <==> maybeNull.isSome()
  maybeNull?.method() <==> maybeNull.map(|v| v.method())
  maybeNull?.method() ?: default <==> maybeNull.map_or(|v| v.method(), default)
The comparison is even more apparent if you're used to Swift, which has a separate Optional type but the same syntactic sugar as Kotlin for manipulating it.

The two ways that Kotlin nullable types differ from Options in other languages is that you have smart casts (if the compiler can prove that the value is never null, you can use it as a normal variable without explicitly unwrapping), and you have automatic coercions from platform types.

Also, I almost never use null in idiomatic Kotlin programming.

> In a sane implementation of Option<T>, there's no space premium for using it over T. The compiler compiles Some<T> to a pointer to T, and it compiles None to null. Swift, Rust, and Haskell all do this; I don't know Scala that well, but I'd imagine it would too unless there's some edge case in Scala<->Java interop.

This is wrong for Scala, and I think it's wrong for the other cases.

> Kotlin's nullable types and Option are exactly the same thing

Except that it doesn't nest properly. Which means it's not compositional: if you write code that works with T?, it will have surprising behaviour if someone ever passes a nullable type for T. And you can't abstract over it, though that's a more general problem of Kotlin not having HKT.

> Also, I almost never use null in idiomatic Kotlin programming.

How do you represent optionality then? I use Option and None pretty often in idiomatic Scala programming (and when I don't it's usually because I'm using a richer variant on the same thing e.g. Either or a custom sum type appropriate to the business requirements) - there's a reason it's such a common example, it comes up in realistic problems all the time.

I think this comment shows a very fundamental lack of understanding how different languages handle errors.

In Java (and Kotlin) -- ignoring the topic of exceptions as an alternative -- errors like "missing value" are communicated with null.

So in Java you first have an error handling problem, you deal with it by returning null ... now you have a null handling problem!

Kotlin tries to put some band-aid around nulls by making them more typed than in Java.

In Scala, errors are handled with bog-standard library types like Option, Either, Try, Validation, not some special language built-in. These types are not facilities to handle null (shown by the fact that all these types happily accept null as a valid value), they are facilities to handle errors.

They handle errors better than Java or Kotlin, because these types allow developers to choose the appropriate type for a specific error case and retain the structure of a computation. To expand on the second point: Nullable types cannot be nested, Scala's types can and regularly are.

This allows Scala to compose operations while carrying errors up to the point where they can be handled easily.

If you have an operation returning an Option[T], and want to run an option on the value returning an Either[S, T] then simply looking at the resulting value will tell you if things succeeded or where exactly things went wrong.

    None               --> First operation did not result in a value
    Some(Left(fail))   --> Second operation failed with cause "fail"
    Some(Right(value)) --> All operations succeeded with result "value"
In Java and Kotlin all you would get would be a bare null, with a probability of people giving up on (typed) null completely and just throwing an (unchecked) exception instead.

TL;DR: Kotlin tries to put some band-aid around Java's broken approach of handling errors with null, Scala deals with errors correctly in the first place.

Outside of very specific low-level operations like Maps or taking .first() of an empty list, you don't handle errors with null in either Java or Kotlin. You handle them with exceptions. You can't ignore the topic of exceptions as an alternative - that is the idiomatic way to signal an error in Java and Kotlin. Nobody seriously considers having high-level APIs return "null" on error; for one, there's no way to differentiate the different things that can go wrong.

Now, there's an alternative school of thought that's gaining currency in languages like Rust, Go, and apparently Scala, and is how we used to handle them in C. That's to return special out-of-band values as part of the result type. The Either type in these languages (realistically, you wouldn't use an Option for any high-level API) in these languages makes this really easy, and sum types in general make pattern-matching on the specific error code & passing back diagnostic information a lot easier than it was in C.

I don't really want to get into a debate about error-by-value vs. error-by-exception, because there are good arguments on both sides of the debate and it's far from a settled question. Personally, if I were starting a new language today, I'd use return values for "expected" error conditions and some sort of panic/recover mechanism to indicate that the programmer forgot to handle a case.

But there's a really strong reason why Kotlin uses exceptions: interoperability with Java code. It's the same reason that Google bans exceptions in their C++ code: you can't really change the error handling mechanism of a large body of existing code, because the existing code's API all assumes certain language conventions, and if you break those conventions, you might as well rebuild the ecosystem from scratch.

So sure, if I were starting a language & ecosystem from the ground up, I'd probably use return values & pattern-matching. But I use Kotlin specifically because I want a "better Java", and need to integrate with Java libraries that may return null and will throw exceptions. If I didn't have those constraints, I'd use Rust instead.

I have seen plenty of libraries using null to indicate an error. If this wasn't the case, as you allege, then why do Kotlin devs act like it's such a big deal? Why add a language feature for something that isn't even considered an issue instead of e.g. improving how Kotlin handles checked exceptions?
One thing I find really frustrating is that Ceylon didn't take advantage of its union types to represent checked exceptions that way :(
Go uses multiple returns for this, IIRC, not an Either type.
> Kotlin's nullable types and Option are exactly the same thing

I don't think that's accurate.

First of all, Scala's Option is a monad, no such thing with Kotlin's nullable type.

And second, Kotlin's nullability is enforced by the compiler so you can't escape it. In Scala, you are welcome to ignore nullable values whenever it pleases you (or when you forget to wrap them in an Option).

They solve a similar problem but with different approaches and different pros and cons.

> First of all, Scala's Option is a monad, no such thing with Kotlin's nullable type.

Is your point that Kotlin's type violates the monad laws? It does, but all that means is that certain constructs behave in really surprising ways and refactors that look obviously safe aren't. I don't see how that could ever be construed as an advantage.

If Kotlin's type conformed to the laws (which would be a great improvement) then it would be a monad even if you ignored that fact. All ignoring it means is that you don't get to take advantage of some common generic functions, but have to write them out separately every time instead.

> And second, Kotlin's nullability is enforced by the compiler so you can't escape it. In Scala, you are welcome to ignore nullable values whenever it pleases you (or when you forget to wrap them in an Option).

In Scala you're forced to check Options (or, better, handle both cases), so it's the same. Yes, if a "bare" null were somehow to get into your program then you would miss it. But where would that null come from? Only from a Java library and that's exactly the same problem in Kotlin. (Theoretically you could also get one coming out of some really bad Scala code, but the library ecosystem knows better than that, and in your own code you either simply don't do that or you use wartremover to enforce that you never do that)

> In Scala you're forced to check Options

But you're never forced to use them. That's the main problem with Scala's approach: it's entirely er... optional.

The Scala compiler will never tell you "You can't write a.foo() because a may be null".

> Only from a Java library and that's exactly the same problem in Kotlin

No, it's not. The Kotlin compiler will not let such values enter Kotlin code until you've told it precisely whether that type is nullable or not.

Scala has no such mechanism so null values can enter Scala code without the compiler having any say over it.

> But you're never forced to use them.

Use wartremover, then you are. Or just never write "null" (or check for it in code review). Yes ideally it just wouldn't be there, but it's really not hard to avoid it.

> The Scala compiler will never tell you "You can't write a.foo() because a may be null".

But you'll never get yourself into a position where a may be null (unless a came from a Java library, but that exact same problem can happen in exactly the same way in Kotlin). If you need to represent absence you'll use Option, and the compiler won't let you write a.foo() if a is an Option.

> No, it's not. The Kotlin compiler will not let such values enter Kotlin code until you've told it precisely whether that type is nullable or not.

No it doesn't. It lets you silently treat a type returned from a Java library as non-nullable.

> Use wartremover, then you are.

Well, yes, that's my point: that the Scala compiler can't do it, so you need an external tool to do it for you.

It's a similar argument to a dynamic language programmer saying their language is statically typed, you just need to use that external tool to type check it.

> But you'll never get yourself into a position where a may be null (unless a came from a Java library, but that exact same problem can happen in exactly the same way in Kotlin)

Again, no it can't.

Types coming from Java are marked as "platform types" by the Kotlin compiler and they are not allowed in the Kotlin code base. You need to convert them to a bona fide Kotlin type first (basically, decide if it's a nullable type or not).

It's the third time I've mentioned this and you keep ignoring it.

In contrast, Scala happily lets Java types enter the Scala object world, along with its null values.

Some(T) is not a pointer to a T in Rust. It's just a T.

If it were a pointer type, then yeah, it would be a pointer.

Presumably it has a different representation from a bare T? E.g. if T=Boolean then there are only two possible values for it, whereas there are three possible values for Option[T], so they need different kinds of storage (obv. in practice a boolean probably doesn't use a whole byte so you can do a kind of packed optimization, but you can't scale that arbitrarily far for deeply nested Option[Option[...Option[T]...], and this is an issue one would expect to hit with an integer or pointer where the natural representation is a full word that doesn't have any "spare" states).
Yes, there's an optimization. Specifically, there's a trait called "NonZero"; if T implements it, then Option<T> uses all zeros to represent None, and so std::mem::size_of::<Option<T>>() == std::mem::size_of::<T>(); If not, then you have to store a tag.
I don't know why you keep coming back to the Java interop aspect:

- It's trivial to solve and really not a problem in practice. I'm pretty sure that when you think about and write code, it's 99% in Scala and very little about Java. "Oh, this value is coming from Java and therefore can be null, I'll wrap it in an Option. Done".

- Kotlin and Scala are exactly on the same level when it comes to interoperating with Java. And I bet we solve it the same way: catch the nullable values from Java as soon as possible before they enter the Scala/Kotlin world, and now we're safe again.

The more interesting case for me is pure Scala or pure Kotlin. And I very much prefer Kotlin's approach to nullability for all the reasons I've enumerated, but let's not go over this again.

Isn't pure Kotlin a much more unlikely scenario than pure Scala? Kotlin devs certainly take pride at every opportunity of how much Kotlin piggybacks on Java, cf. collections.

I think you can't have it both ways, having a worse abstraction for handling errors while claiming that it works better with Java code and then claiming that "pure Kotlin code" is where the more interesting case is.

No, JetBrains is actually working on Kotlin Native [1].

[1] https://kotlinlang.slack.com/messages/kotlin-native/

Yes, exactly, which makes the claim even more baffling.

Sure, they can just reimplement all their Java dependencies, but if they have to implement e.g. collections anyway, they could just have designed a better API in the first place, instead of being stuck with Java collections on platforms that don't even run Java.

> - It's trivial to solve and really not a problem in practice. I'm pretty sure that when you think about and write code, it's 99% in Scala and very little about Java. "Oh, this value is coming from Java and therefore can be null, I'll wrap it in an Option. Done".

That works for values coming out of Java but not for values going into Java.