Nullable<T> instead of Option<T>

I'd recommend checking the link that I'd included in my reply. It seems you're trying to re-explain the same thing here.

I don't think by seeing Nullable a programmer would suddenly think values can be null in Rust and can cause runtime errors like java. Actually I'd say that he'd think when x is wrapped in Nullable<x> that means x can't be null itself. Exactly like value types in C# which can be wrapped in Nullable<>, while reference types can't.

The fact that Option exists in OCaml doesn't make it a good name. optional is a good and correct name, Option isn't.

I wanted to highlight this specifically, as the most important thing for every change idea, language or library.

Clearly the person opening a thread believes that there's a problem to be solved. But they first need to convince the readers of that -- in many cases, that's the hard part, not the implementation.

This is why Motivation is the first section in RFCs, not technical details. This is why libs-api has added ACPs to talk about the purpose of a change, needing only a sketch of a solution, before PRs for new APIs can be accepted. Lang has talked about trying for a similar split, where the team would approve "here's a problem worth tacking" separate from the actual solution.

11 Likes

Yes, I am, because this is critical to why it's important to realise that Option and @Nullable are very different concepts. One is an annotation that's ignored by the compiler with the right settings, the other is a part of the type system that cannot be ignored.

The fact that you're posting that link, and still saying that Option and @Nullable are basically the same thing implies to me that you haven't understood what the link is trying to tell you about the differences between the two, and without some guidance from you on why you see the two as "basically the same", I can't do a better job of explaining the distinction.

They might if they're a C# programmer. But a Java programmer knows that all reference types are inherently Nullable, and that @Nullable is just an annotation to tell static analysis tools that you expect this value to be null in some valid executions of the code.

The entire point behind using a name that isn't the same as Java or C# is that the semantics are different (and the same as OCaml), and thus that if you're coming from Java or C#, while "this is basically Nullable under a different name" is a reasonable first pass understanding of Option, it's not the full story, and it's not accurate in all sorts of cases that you might care about.

Your C# example gives another reason why it's potentially confusing to a Rust learner - in Rust, all types can be wrapped in Option, while in C#, you've just said that some can and some can't. Again, by using a different name, you can avoid people bringing along expectations from elsewhere, since they can clearly see that it's different, even if they don't yet understand the reasons why it's different.

Again, the point is not that they're radically different - the point is that they're different enough concepts that a learner coming from Java or C# needs to be aware that this is an area of divergence from their expertise, and that Option is not an annotation, nor is it a way of opting certain types into the "normal" behaviour of other types, but it's just a type in its own right with no special rules.

And Option as just a normal type is surprising compared to nullability if you're used to the rules of C++, JavaScript, Java, C#, or many other languages. There are consequences from this decision that mean that Option<&str> in Rust is not the same as String in Java, despite having similar semantic meanings to the compiler (an optional string).

4 Likes

I have my own complaints about the naming of things (Vec is my biggest peeve), but Option seems like a very accurate name to me. If a function returns Option<String>, it has an option to return a string, or it may not return it. Nullable<String> makes less sense because there is no such thing as a "null string".

3 Likes

I wanted to add something to this discussion that took me a while to realize. Among all the "things to learn" in Rust, discovering helpful functions in the standard library was a bigger one than I anticipated. (I needed to read the docs, but I hadn't)

After discovering the many helpful methods on Option, notably Option::map, you can start to structure code in a way where receiver functions that require a type T don't need to deal with the Option<T> that you may have.

So this setup...

...can be defined instead as accepting i32 and called via

let input= Some(1);
let incremented = input.map(plus_one);

playground (On mobile, apologies for typos)

5 Likes

Or, soon™,

let incremented = try { plus_one(input?) };
2 Likes

NOT A CONTRIBUTION

Structuring your code this way actually has a perhaps surprising benefit: it alters the failure modes of that particular function.

Specifically, it reduces them i.e. the original fn takes and returns Option<i32>, meaning there is nothing preventing it from turning Some(42) into None, whereas in the version dealing in i32, you get a number out, guaranteed.

3 Likes

Which code is more readable?

let greeting_file = File::open("hello.txt")
        .panic_on_error_with("hello.txt not found.");
let greeting_file = File::open("hello.txt").panic_on_error();

or

let greeting_file = File::open("hello.txt")
        .expect("hello.txt not found.");
let greeting_file = File::open("hello.txt").unwrap();

But .expect("") and .unwrap() don't just panic on error. They also, and in most cases, return the value that was wrapped by the Ok variant—which is actually their primary purpose.

1 Like

Ditto, The history of StandardML indicates that the initial language definition, did not have the option type. Appears to have been introduced with the initial basis library around 1991, unless the designers of the basis library also got it from somewhere...

4 Likes

The thing I love about Rust (and Dart, to lesser extent), in this regard - is that this is doable in 2 minutes:

Summary
#[derive(Debug)]
enum Nullable<T> {
  Null,
  Value(T),
}

fn _main() {
  // too horribly looking?
  let f_option = Some(false);
  // no worries, you can do this:
  let nullable = f_option.into();
  // another option?
  let another_f_option = best_fn_ever(nullable);
  // no problem
  let n = Nullable::from(another_f_option);
  // debuggable nullable, yay
  println!("{:#?}", n);
}

fn best_fn_ever(b: Nullable<bool>) -> Option<String> {
  let back = if let Nullable::Value(true) = b {
    "life's awesome!"
  } else {
    "life's HORRID"
  };
  Some(String::from(back))
}

// THE MAGIC

impl<T> From<Nullable<T>> for Option<T> {
  fn from(n: Nullable<T>) -> Self {
    match n {
      Nullable::Value(v) => Some(v),
      Nullable::Null => None
    }
  }
}

impl<T> From<Option<T>> for Nullable<T> {
  fn from(o: Option<T>) -> Self {
    o.map(|some| Nullable::Value(some))
      .unwrap_or(Nullable::Null)
  }
}

You can define and implement custom IntoNullable traits for Result and Option with a trivial nullable() method that will always return your favorite Nullable for every Result / Option out there, wrap your own Nullable into a Result or Option for those few pesky moments when you do need to pass them as arguments to another core library - and you won't have to deal with Some or None ever again.

At the same time, none of the existing code that might be running and diligently servicing millions of people around the world will have to be refactored simply because you happen to be a bit more familiar with, and have a bit of a soft spot for, another syntax. It's a win-win for everyone.

(sarcasm is in my blood, don't take it personally)

Some things, ultimately, don't matter that much. What you do with those Nullable's and Option's is much more important than the exact combinations of keystrokes needed to instruct the compiler to, at the very end of it all, to do the exact same thing behind the scenes.

Same goes for panic_on_error_with vs expect. I, personally, hate the unwrap word - and I couldn't, in a million years, tell you why. I also hate the Result<()>, Ok(()) and the () unit themselves. They are just .. mildly annoying to look at. Guess what? Every project of mine starts with type Result<T=()> = std::io::Result<T> and const Done: Result = Ok(()). There. I wrote it once and I never touch it.

Then, in main(), I can happily go:

fn main() -> Result {
  ...
  Done
}

Because I want to, because I can, and because that's more readable for me - but would I ever ask the core team itself to get rid of unit and replace it with void (which I do happen to like a bit better myself)? Not in a million years. It does what it's supposed to do, it's there, so leave it be.

I would think some compile-time detection of Option<T> (or Nullable<T> - whatever rocks your boat) in the parameter declaration of a given fn:

fn call_me(number: u8, just_kidding: Option<bool>) { ...

In order to be able to just call_me(0) instead of call_me(0, None) - would be awesome. But that's a totally different matter and would require a whole new topic to discuss all the pro's and con's.

TL;DR - dude, it's just naming. Wrap it in your own custom trait's - if you must - and forget about it

3 Likes

In your projects, you should never write a wrapper for something that is built-in or defined in the standard library, just because you don't like its name. That may make you happy, but will make your coworkers and anyone who reads your code very sad.
That's why the authors of standard libraries should be extra careful in choosing names. The community of a language has to live with those names forever.

I don't know if I was able to explain my point or not. Something like Option or unwrap becomes a part of the language and if you change them you'll just confuse anyone who reads your code. I can use an alias like this:

use std::option::Option as Nullable;
use std::option::Option::Some as Value;
use std::option::Option::None as Null;

but that will only cause confusion and trouble because people are familiar with the official names not these ones.

Instead, you should redefine your whole standard library each and every time someone comes along to point out that in their personal, subjective opinion, the naming choice that you made for your std is sub-par? Regardless of the impact it might have on existing code, all the people being perfectly happy with (or just plain neutral and fine with the status-quo of) current naming system? Gosh, that would be fun.

I'd love to see this tried as an experiment, to be honest - just to see how much time would have to be spent ironing out each and every complaint of this kind, versus working on incorporating and stabilizing new features. My own bias strongly suggests it would end up looking more horrid than PHP - very soon, and that it would quickly get abandoned and forgotten, the way it should be. You can't please everyone.

Win32api is literally all wrappers, with dozens of definitions for the exact same void *. Named in the most horrid (in my own, highly subjective opinion) Hungarian notation with LPCWSTR-like abominations. Sure, you might argue they didn't wrap them up just because they didn't "like" the null pointer - but what's the precise line in between usable aliases and preference-based wrappers? Will you be the one to draw it?

Quite a strong stance, right there. You are perfectly aware of the fact that each and every company is free to adopt their own coding style and enforce it as they see fit - as long as it's documented and clearly outlined, people can quickly jump on the train after skimming through the general reference. Aren't you?

In Rust especially, given the extra-amazing macro system, most of those who know their way around the macro_rules! can quickly define their own micro-DSL's. Are you against macro-ing your code as well, just to make sure no one gets accidentally sad if they have to spend an extra minute figuring out what the author had in mind? Seems to be me, personally, as too much of a restriction for it to be practical.

Correct! That's precisely what the rest of people here have been trying to Tx across your own Rx on the other side of your ASCII-into-meaning decoding program: changing it now would lead to confusion, breaking changes, maintainability issues, countless other coming along at the Rust team with:

Our whole infrastructure collapsed. Thank you for the Nullable<T>, though.

And all for what?

If you can't see yourself using the Option the way I can't bring myself to type Ok(()) - just alias it up and be done with it. Aliases are even better for your case, given the naming-only concern. That should have my own first instinctive reaction the first time I saw it, now that I think about it. Ah, well.

TL;DR - it's still just naming; and your alias solution is perfect for your case.

This discussion seems to be leading nowhere, I’m closing this thread.

As I already mentioned above, I think the topic whether or not “Option” should be re-named into “Nullable” has been discussed here sufficiently. Of course, anyone can feel free to open a new thread either on IRLO or on URLO for any of the side-discussions that seem worth continuing; in particular discussions that were not about renaming “Option” into “Nullable” (or renaming other established standard library items).