Stream: general

Topic: Avoiding unwrap in an Option pattern


Josh Triplett (May 10 2020 at 15:15, on Zulip):

Is there an unwrap-free way to write this code?

an_option = Some(compute_thing()?);
an_option.as_ref().unwrap()
Josh Triplett (May 10 2020 at 15:16, on Zulip):

Semantically, "put a value into an Option and give me a reference to that value".

simulacrum (May 10 2020 at 15:18, on Zulip):

@Josh Triplett https://doc.rust-lang.org/nightly/std/option/enum.Option.html#method.get_or_insert I think?

simulacrum (May 10 2020 at 15:19, on Zulip):

but not in the language -- that has to use unreachable_unchecked to avoid an unwrap https://doc.rust-lang.org/nightly/src/core/option.rs.html#865

lcnr (May 10 2020 at 15:19, on Zulip):

That's a really nice problem :heart: I don't think so as the value has to be moved into Some, during which it can't be referenced

Josh Triplett (May 10 2020 at 15:21, on Zulip):

@simulacrum Interesting. Wouldn't have thought of using get_or_insert for a case where I know the value is None.

Josh Triplett (May 10 2020 at 15:21, on Zulip):

Thanks, that should work.

lcnr (May 10 2020 at 15:22, on Zulip):

None.get_or_insert(value) looks really weird :sweat_smile:

simulacrum (May 10 2020 at 15:22, on Zulip):

We could expose a set(&mut self, value: T) -> &mut T API as well which would avoid the initial branch, but I doubt you care

Josh Triplett (May 10 2020 at 15:23, on Zulip):

@simulacrum 1) I really don't, and 2) in an ideal world, rust can just inline get_or_insert and notice statically that the value must be None, and thus avoid the branch.

simulacrum (May 10 2020 at 15:23, on Zulip):

ah, well, presuming that's statically known then yes

simulacrum (May 10 2020 at 15:23, on Zulip):

(generally my code where I "know" such things it's because there's code far away that "knows" them)

simulacrum (May 10 2020 at 15:25, on Zulip):

It does look like in the trivial case at least we do skip the branch https://rust.godbolt.org/z/hEAyqw

Josh Triplett (May 10 2020 at 15:25, on Zulip):

The pattern here is "get thing out of LRU cache if it's there, if not, go get the uncached thing expensively, either way I need a reference, then later I want to stuff any uncached thing into the LRU cache".

Josh Triplett (May 10 2020 at 15:26, on Zulip):

So an_option here is uncached_thing, which I've just created, and which contains None unless I had to go expensively retrieve the thing.

simulacrum (May 10 2020 at 15:26, on Zulip):

makes sense

Josh Triplett (May 10 2020 at 15:27, on Zulip):

Also, thanks for testing the optimization there. I'm always impressed with the degree to which Rust makes sensible code patterns work well without having to make them less sensible.

simulacrum (May 10 2020 at 15:28, on Zulip):

fwiw it looks like the as_ref().unwrap() also optimizes away to same assembly

Josh Triplett (May 10 2020 at 15:29, on Zulip):

I had little doubt that it would, but I don't like writing unwrap if I don't absolutely have to.

Josh Triplett (May 10 2020 at 15:29, on Zulip):

As far as I'm concerned, unwrap has a proof obligation just like unsafe does.

simulacrum (May 10 2020 at 15:30, on Zulip):

yeah, totally can understand that :)

Josh Triplett (May 10 2020 at 15:30, on Zulip):

Thanks for the help! I'm going to go with the get_or_insert solution.

Josh Triplett (May 10 2020 at 15:31, on Zulip):

It helps that I can avoid get_or_insert_with because I know the function would always be called.

simulacrum (May 10 2020 at 15:31, on Zulip):

I sort of wish that we had a way to express this in the language itself -- essentially getting access to the "slot" of Some(T) after storing into it -- but there's no nice way to express that even syntax wise

Josh Triplett (May 10 2020 at 15:32, on Zulip):

@simulacrum I know what you mean, and I was thinking the same thing when I posted this question. Hard to articulate what it means, but there's an intuitive notion.

Josh Triplett (May 10 2020 at 15:33, on Zulip):

I guess &mut T is pretty close to "give me access to the "slot" of a Some."

Josh Triplett (May 10 2020 at 15:33, on Zulip):

An exclusive reference to a location.

simulacrum (May 10 2020 at 15:34, on Zulip):

Yes, I've contemplated some syntax to give you &mut uninit T basically to an Option's Some slot such that you can write to it and then the Option becomes Some and you can just &* your prior reference to get a &T to the slot

simulacrum (May 10 2020 at 15:35, on Zulip):

but there's no way today to get a pointer to the Some slot of an Option without actually, well, making it Some and doing a match or w/e

Josh Triplett (May 10 2020 at 15:35, on Zulip):

That's interesting!

simulacrum (May 10 2020 at 15:36, on Zulip):

this is very specifically tailored to option, in some sense, and it gets into the enum-variants-as-types rfc(s), too

Josh Triplett (May 10 2020 at 15:36, on Zulip):

In theory, we could easily add this function to Option<T>: fn (&mut self) -> &mut MaybeUninit<T>.

simulacrum (May 10 2020 at 15:37, on Zulip):

yes, but that doesn't do what you need it to -- the key bit is that initialization would also write the enum tag bit/byte

Josh Triplett (May 10 2020 at 15:37, on Zulip):

Ah. Such that if you don't write to the value, it isn't a Some?

simulacrum (May 10 2020 at 15:37, on Zulip):

right, yes

Josh Triplett (May 10 2020 at 15:37, on Zulip):

Now that's fascinating.

Josh Triplett (May 10 2020 at 15:38, on Zulip):

How would that even work? That would mean that it isn't an ordinary "place", because assigning to it would also have a side effect.

simulacrum (May 10 2020 at 15:38, on Zulip):

it's basically something like "DerefMut" but with a "destructor" on the returned mut

Josh Triplett (May 10 2020 at 15:39, on Zulip):

"destructor", or "constructor"? It takes effect when given a value, right?

simulacrum (May 10 2020 at 15:39, on Zulip):

well, perhaps, or perhaps when the &mut T ends

simulacrum (May 10 2020 at 15:40, on Zulip):

(I guess it wouldn't really be possible to observe which of those it is)

simulacrum (May 10 2020 at 15:40, on Zulip):

in some sense you can get this today with DerefMut if your option has a default value you're willing to let it take on if you fail to actually write

simulacrum (May 10 2020 at 15:41, on Zulip):

e.g. we could do DerefMut for Option that returned &mut T and initialized to T::default() prior to returning that ref

simulacrum (May 10 2020 at 15:42, on Zulip):

if we had such "guaranteed write" references we'd also be able to use them I think with some work for Read's uninitialized buffers problem

simulacrum (May 10 2020 at 15:42, on Zulip):

but I have no sense of how we'd actually encode what they meant

simulacrum (May 10 2020 at 15:43, on Zulip):

i.e. what conditions they impose -- just saying "you must write to this" seems easy but what that actually means in a more spec-y way I haven't been able to come up with

Josh Triplett (May 10 2020 at 15:44, on Zulip):

A "guaranteed write" reference would have similar obligations to an uninitialized let?

Josh Triplett (May 10 2020 at 15:45, on Zulip):

"the compiler must statically know you write to this before you read it"?

simulacrum (May 10 2020 at 15:45, on Zulip):

kind of, but it's harder in the sense that you probably want to be able to pass it to some function

simulacrum (May 10 2020 at 15:45, on Zulip):

you can't actually do anything pretty much with an uninitalized let

simulacrum (May 10 2020 at 15:45, on Zulip):

whereas here you want to be able to carry that proof obligation downwards for a bit

Josh Triplett (May 10 2020 at 15:49, on Zulip):

Well, in the meantime, hyperfine thanks you for your incremental contribution to performance. :)

Josh Triplett (May 10 2020 at 15:50, on Zulip):

Not having to pop a value out of the LRU cache and put it back later seems to have saved a few tens of milliseconds over my standard six-second run.

Josh Triplett (May 10 2020 at 15:54, on Zulip):

It's fun doing performance optimization on software that runs operations millions of times. Tiny tweaks give huge wins.

Josh Triplett (May 10 2020 at 16:01, on Zulip):

On a separate note, I'm eagerly awaiting the ability to write if condition && let Some(x) = y {.

Josh Triplett (May 10 2020 at 16:03, on Zulip):

Because right now I have to write:

if condition {
    if let Some(cached_thing) = thing_cache.get(&key) {
        cached_thing
    } else {
        uncached_thing.get_or_insert(complex_get_thing()?)
    }
} else {
    uncached_thing.get_or_insert(complex_get_thing()?)
}
Mark Drobnak (May 10 2020 at 17:03, on Zulip):

You can simplify this with a match:

fn main() {
    let condition = true;
    let option = Some(0);

    match option {
        Some(value) if condition => println!("Some && condition"),
        _ => println!("None || !condition"),
    }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2a10d5ef9a4495efcc1c3433eef2eb09

Last update: Jun 05 2020 at 23:10UTC