Hacker News new | past | comments | ask | show | jobs | submit login

Is there a difference between excluding null from a type and having basically syntactic sugar for ‘T | Null’ in the form of ‘T?’ ?



Yes. Having an explicit `Option<T>` type allows you to write things like `Option<Option<int>>`. This type is not possible in, say, Kotlin or Typescript. (Well, technically it is, but not using the native null types provided by the language.)

Most of the time this isn't a very useful type to have, but it has the big advantage of being mechanically obvious. For example, consider a hashmap of optional types, something like `Map<String, Option<Player>>`. If we write a get method for the map, it should return an Option to indicate whether the value was present or not. But what happens if the value is present, but it is explicitly Null?

The advantage of having an explicit option type is basically that it's easier to compose generics without having to understand what the values might be, which is usually what you want when using generics. That said, most of the time, if you've got an option of options of something, you're just going to flatten that type down anyway.

E: Thinking about it, the other advantage is that it's easier to create a separate namespace for "methods that should exist when T might be null but are meaningless the rest of the time". For example, Rust's `Option` type has methods to map the internal value into another value, unwrap the value and panic if it was null, swap the value with a different one, etc. In languages which use null | T, the equivalent is usually to use Elvis operators and similar (obj?.field), but that requires more special casing.


I guess one could have ‘(T | None) | None’, which can automatically be flattened to ‘T | None’, couldn’t it?

Also, the Map::get method that can return null is the problem here (even though your example is great and thanks for that, I didn’t think of it), something like ‘Option<Player?>’ should allow to differentiate between those cases.

But I do remember reading that a bottom type does make typing rules harder (e.g. scala 3’s explicit nulls feature which is basically “T is non-nullable, write it as T | Null” is not sound)


The other commenter related the types to normal binary operations, which I think explains why (T | Null) | Null can't be distinguished from T | (Null | Null). Another way of thinking about it is by asking what "untagged unions" actually means, and in the context of languages like Typescript, it generally means "unions where the tag is the type of the object". ("Untagged" here is a bit of a misnomer.) But if the tag is the type of the object, how do I distinguish between two different nulls? They both have the same type (the Null-Type), which means they both have the same tag in the union, which means they're the same.

What you say about `Option<Player?>` is a good point though. If we want to distinguish between the different results, we need an explicit Option type. But now we've got Option _and_ we've got nullable types. Which should we use? They're both doing the same thing (i.e. marking where a type may be present but might not be), so why do we need both?

In practice, my impression that nullable types are really good for integrating with languages that already have unchecked nulls in then, either for historical reasons (like Java) or because they're dynamic languages (like Python or Javascript). It's a way of acknowledging the null value in the type system without demanding that all the code that been interacting with nulls be rewritten.

However, if you were going to write your own language from scratch, it's difficult to see why you would allow nullables to exist when the Option type does pretty much everything that nullables can, but with more clarity for cases of "nested nullability". You can also still add syntax sugar for it (Rust is going down this route, for example), but you don't have to special case nullability to the same extent.


Thanks for the answer, I was genuinely curious about the advantages/disadvantages of these approaches. But indeed, types that completely exclude ‘nulls’ seem to be the cleaner solution — though I wonder whether that is even possible to retrofit to Java.


No, you cannot.

var foo: (T | None) | None = None

Which None do you mean here?

This is just basic logic: (X or Y) or Y <=> X or Y

Untagged unions cannot model that. Contrast it with tagged unions:

var foo: Option<Option<T>> = Some(None)

var bar: Option<Option<T>> = None

Now you can reason about where the None means.


I may have not been clear, but that “flattening” is what I meant, and mentioned it as a “feature” (when Some(None) and None is equally useful/useless).

Surely, if you want to distinguish between 3 states you need more data, hence my recommendation of Option<T?>




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: