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

That’s why I said properly implemented! Good optional implementations let you do the check once to unwrap the inner value, so you have one branch.

In rust:

    fn foo(s: Option<&S>) -> {
      if (let Some(inner) = s) {
        // now you can access inner repeatedly
        // without branches
      }
    }
(edit to fix syntax errors)



but this declares a new variable - I find this super messy and really decreases readability in practice since now it's not obvious anymore that what you're working with was a function parameter. Also it adds a scope level - I much prefer the

   if(!bla)
     return;

   // use bla
early-return style. So that's really a no-go for me from years comparing the two styles


Well if you have aesthetic objections to the way rust does it, I can't argue with you. Kotlin does it the way you like, though.

Anyway, I started in on this thread because I was objecting to the claim that optional types always have overhead. They don't. That's all I wanted to show.


  > Well if you have aesthetic objections to the way rust does it,
I agree with GP that it is harder to maintain ("messy" they say) if there is more than one variable referring to the same value. It is not so subjective as you make it to be.


The idiomatic way to solve that in rust is to re-bind to the same variable name.

   if let Some(foo) = foo { /* ... * / }
That's possible because in Rust name shadowing

    let foo = grab_foo_bytes();
    let foo = parse_foo_bytes(foo);
makes the previous binding of the variable no longer namable and thus no longer accessible, but doesn't drop it (and trigger RAII destructors).

Now someone will probably come in and say "oh no, this isn't exactly like c, how will anyone ever understand it". To that I reply why is it that c users get to say "if you don't know how c works exactly you're holding it wrong" and then comment about other languages "I don't want to have to learn anything to hold it right".


  > The idiomatic way to solve that in rust is to re-bind to the same variable name.
OK, that's reasonable. Is the idiomatic way to use optionals to introduce a layer of nesting? I prefer keeping functions very "flat"-looking. It sounds like Rust's optionals will give people an excuse to create labyrinthine functions where I'm constantly scrolling around to remind myself of what level of nesting I'm at and whether I'm in a loop or not, etc.

As you say, you don't need a new block to shadow a previous var, so hopefully that style catches on.

  > "oh no, this isn't exactly like c, how will anyone ever understand it"
Not very persuasive, sure, but the network effect of the C/C++ culture (including its general syntax and imperative nature) is a strength in and of itself. New languages would do well to coddle the existing C++, Java et. al. users wherever it doesn't contradict the language's central mission.


> I prefer keeping functions very "flat"-looking.

I definitely agree. One way I do that is by having an internal function that takes a valid value and a public function that does the validating/error handling.

That doesn't always make sense though. There's a few other idiomatic ways to avoid nesting. Since statements evaluate to values, you can write

    let foo = if let Some(foo) = foo {
        foo
    } else {
        // Something that either evaluates to the same type as foo or returns early
    }
That's so common there's a special operator for it, ?. It essentially either early returns the sad path or evaluates to the happy path.

    fn get_foo() -> Option<Foo>;

    fn frob() -> Option<Bar> {
        let foo = get_foo()?;
        let bar = convert_to_bar(foo);
        Some(bar)
    }
I prefer to use Result to model missing data like cases instead of Option because it composes better. So that might be

    fn get_foo() -> Option<Foo>;

    fn frob() -> Result<Bar, BarNotFound> {
        let foo = get_foo().ok_or(BarNotFound)?;
        let bar = convert_to_bar(foo);
        Some(bar)
    }


    #[derive(Debug, thiserror::Error)]
    #[error("Bar not found")]
    struct BarNotFound;
That last bit uses a stdlib macro and a very commonly used external lib macro to save a few lines of repetitive typing.

Edit: Also ? doesn't special case Result and Option. You can make your own type conform to the interface (trait) it requires. That would probably be weird though.


Interesting. Thank you for sharing.


It's exactly like Lisp:

  (let* ((x (this))
         (x (that))
         ...)
    ...)
In C you cannot even do:

  { int x = 42;
    { int x = x + 1; // x + 1 refers to this second x
      ... } }
This is because the scope of the identifier being declared already starts at the =. So even if the redeclaration were allowed without opening a new block scope, it wouldn't work.

However, there is a good reason for that: initializers can be self-referential, so they have to have their own identifier in scope:

   // define circular structure in one step, no assignments:
   struct node n = { .next = &n, .prev = &n };
In this regard, the scoping rule is like letrec in Scheme or labels in Common Lisp.




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

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

Search: