Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative.
Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative.
Isn’t that the point of Optional? It just moves that identification of those error states to compile-time rather than at runtime.
The only sane approach is removing all nulls as early as they come and declaring/initializing all member variable in the c-tors.
The ideal state is that anything that isn’t Optional is guaranteed to be non-null. So when you go ahead and remove all nulls as early as possible, you can statically guarantee that remains true forever-more.
But if all your libraries just go ahead and drop this knowledge anyways, and when they return an object without meaning non-nullable, then you’re back to where you started.
Whereas in an ML, I know every library is already adhering to this, so something being not-optional actually means something. Bolting it into a language ad-hoc, you go from everything can be null to this weird tristate — a type might be nullable, or might not be nullable and if it’s in an optional, it definitely is nullable… so your best bet is just checking for null every time anyways
> Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative.
Yeah, and with Optional you would have found it at compile-time.
so we put on our big-dev pants and build all sorts of structures around handling errors to make it more sound/ergonomic.
but at the end of the day:
o its still very difficult to respond in a semantically meaningful way to errors as code
o they still almost always just get dumped into a log or ignored, or just result in a panic
o even if we have managed to surface them - they still aren't really that actionable by the end user
so we haven't helped the situation much, but some of these answer like catch/throw have really unpleasant consequences - we may have made it worseScala's ZIO is one of the better examples if you are interested to look into an alternative.
I kind of think that sentence is contrary to the point of Optional, and if you start from that mindset then Optionals do end up no better than nulls.
Instead you enforce good hygiene where you unwrap early and pass non-Optionals for anything that you "know" is non-null. If you call a function which returns optional, and you're find yourself thinking "nah, I _know_ this is definietly non-empty" then you're incorrect; the API explicitly is telling you that it definitely can be empty.
If you just do "Map.get(x)" and you get null because your thinking was wrong, then the NPE will pop up potentially 10 layers later or an hour later, when the null-value was tried to be used in your program.
On the other hand, with "Map.get(x).orElseThrow()" you will have an exception thrown immediately! Even better, you should customize that as "Map.get(x).orElseThrow(NoSuchElementException(x))" so that you know the value that was unexpectedly not in the list.
That will make debugging much easier and will potentially fail a test case while a "Map.get(x)" might not cause the test to fail.
case class Address(street: String, city: String, state, String) case class User(id: Int, name: String, address: Option[Address])
def filterCAUsers(users: List[User]): List[User] = users.filter(_.address.exists(_.state != "CA"))
If you just want to provide a hint to the caller at compile time about whether you intend for the possibility of returning null, you could use @Nullable/@NotNull annotations to achieve that with no overhead.
E.g. you might want to return the streetnumber of a user's address. When a NPE is thrown, was it because there is a bug somewhere in the code or because the user's address has no street number?
With Optional, it is clear: there was no street number.
Option types are 8 years older than null (ML: 1973, null: 1965). There's nothing clever about it. It's simple to understand, and is an explicit wrapper around an object, while a pointer is an "implicit" pointer around an object. Option<Obj> is clear, you know that it can be something or nothing. Plain Obj is deceiving.
> Every NullPointerExceptipn my code ever caused was good, because it helped find an error state that had no acceptable default non error state alternative.
That's the point of option types and pattern matching, it can force you to handle both cases.
Since I started using "safe" nulls with Ceylon, nearly a decade ago, later with Kotlin and TypeScript, I've been firmly in the camp of supporting nulls in the language instead of `Optional` or `Maybe`. Rust should've used them IMO... using `?` for error propagation instead is such a weird choice.
Example (from [1]):
// Assume
// fn halves_if_even(i: i32) -> Option<i32>
fn do_the_thing(i: i32) -> Option<i32> {
let i = halves_if_even(i)?;
// use `i`
}
Besides, the `?` is syntax sugar for the `try!` macro as explained in the `try!` macro docs [2], and has nothing to do with sum types, at least not directly.Ceylon would be a closer case where nullable types are represented as sum types and not a special-case in the language (`String | Null` is the same as `String?`)... but Ceylon, as Rust, special-cased the `?` operator for "something"... in Ceylon, they are used for null-checks, and in Rust, for propagating errors via the `try!` macro, which is what my previous comment was about.
[1] https://stackoverflow.com/questions/42917566/what-is-this-qu...
> Ceylon would be a closer case where nullable types are represented as sum types and not a special-case in the language (`String | Null` is the same as `String?`).
Aren't those union types? My understanding is that they are different from sum types, as you usually have to declare the sum types beforehand, and sum types are collections of values, while union types are collections of types, and are often declared "on the spot". At least that's how they work in Typescript, I don't know much about Ceylon.
To go back to option types, people usually like them because there are well-known patterns to program with them (like map). Of course you can get the same safety with static flow analysis (Typescript does this), but I think part of it is ergonomics, influenced by functional programming. Though if there is an overhead like in Java, I'm not sure about their value.
Ceylon:
alias StringOrInt = String | Integer;
StringOrInt myValue = "foo";
StringOrInt otherValue = 10;
Rust: enum StringOrInt {
Str(String),
Int(usize),
}
let my_value: StringOrInt = StringOrInt::Str("foo");
let other_value: StringOrInt = StringOrInt::Int(10);
In Ceylon, you're right you don't need to name a "union", which IMO makes it much nicer, as I was able to use adhoc unions everywhere, e.g. inside streaming operations: [ for x in foos if (x.something()) "foo" else 10 ]
The above "automatically" has type `[String | Integer]`. In Rust (and Kotlin) I would need to name a type just for this.Another disadvantage of Rust's approach is that the type variants are not types themselves. That's really annoying sometimes, e.g. you want to have a method that only handles one of the variants, but you can't (you need to pass the values the variant wraps separately, but that may be undesirable semantically as that would allow something that has no instance of the enum to also use the function).
As an interesting aside, Rust has what seem to be intersection types for trait bounds (fn toto<T: Trait1 + Trait2)(a: T)).
I'm still not sure about the tradeoffs between union/intersection types and sum/product types. I think the difference is that "set types" are structural, while algebraic types are nominal, which comes with the usual tradeoffs, but I'm not sure.
It is like Google uses their pseudo Java dialect when selling Kotlin features, instead of comparing it with proper Java.