rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Option<T> / Result<T, E> rewrap shorthand

Open semirix opened this issue 4 years ago • 14 comments

The problem

In code I find myself having to do a lot of:

let b = if let Some(a) = a {
    if let Some(b) = a.b {
        Some(b)
    }
} else {
    None
}

What's available right now

Currently I can do this:

let b = a?.b;

But I can only use the ? operator from a function that returns an Option<T>. In the vast majority of cases I'm returning a Result<T, E> so I can't use the existing shorthand. Additionally, this would trigger an early return where I might want to continue working with the unwrapped value.

The Proposal

A syntax shorthand that conditionally unwraps values to access deeply nested values behind myriad Option<T> and Result<T, E>. This syntax does not do an early return like the ? operator, it only produces a value. This behaves very similarly to JavaScript's optional chaining operator: ?.. An example of this could look like:

let b: Option<B> = a!.b!;

The behaviour for Option<T> and Result<T, E> follows like so:

Returning Option<T>

struct A {
    b: Option<B>
}

struct B {
    c: Option<C>
}

struct C;

fn get_c(a: Option<A>) -> Option<C> {
    a!.b!.c!
}

This example returns an Option<T> at the end where T is the field value. The field value can either be T or Option<T>.

Returning Result<T, E>

struct EA;
struct EB;
struct EC;

struct A {
    b: Result<B, ErrorB>
}

struct B {
    c: Result<C, EC>
}

struct C;

fn try_get_c(a: Result<A: EA>) -> Result<C: EA> {
    a!.b!.c!
}

In this scenario, the root level E type is always returned in the Result<T, E>, which in this case is EA. This is meant to behave exactly as the ? operator does in doing early returns in fn() -> Result<T, E> functions. Therefore, for this to not error we need to have the appropriate From<T> traits implemented for our error types like so:

impl From<EB> for EA { ... }
impl From<EC> for EB { ... }

Conclusion

I suspect there will be a few gaps or problems with implementing this that I have not been able to foresee but I believe a syntax shorthand like this will make working with Option<T> and Result<T, E> much more terse and a great deal quicker.

semirix avatar Jan 25 '22 06:01 semirix

You could just use Option::and_then():

fn get_c(a: Option<A>) -> Option<C> {
    a.and_then(|a| a.b).and_then(|b| b.c)
}

Edit: Even if this function were returning a result and ? could not be used.

hkratz avatar Jan 25 '22 07:01 hkratz

No need for new syntax, the following works:

fn get_c(a: Option<A>) -> Option<C> {
    a?.b?.c
}

lebensterben avatar Jan 25 '22 08:01 lebensterben

this would trigger an early return where I might want to continue working

Use an IIFE (in stable) or a try block (in nightly):

fn get_c(a: Option<A>) {
    let v = (|| a?.b?.c)();
}
#![feature(try_blocks)]

fn get_c(a: Option<A>) {
    let v: Option<C> = try { a?.b?.c? };
}

The same idea applies to Result and any type that implements Try:

fn try_get_c(a: Result<A, EA>) {
    let _: Result<C, EA> = (|| Ok(a?.b?.c?))();
}

shepmaster avatar Jan 25 '22 13:01 shepmaster

@shepmaster The try { ... } block is the closest to what I idealised. I was unaware try was in nightly. If it gets stabilised, this proposal in my eyes goes from a QoL improvement to a nice-to-have as try { a?.b?.c? } is a clean solution in my eyes.

semirix avatar Jan 27 '22 01:01 semirix

I'll add that try blocks cover this in a bunch of other scenarios where I wouldn't know where to put the ! proposed here. For example, Option<A>, Option<B> -> Option<(A, B)> is try { (a?, b?) }, which I find pretty elegant.

scottmcm avatar Feb 01 '22 07:02 scottmcm

+1 on the general idea. I'm frequently running into cases where it would be nice to have a quick (with as little code as possible) "non-returning" version of ?.

I'm aware of Option::and_then() but it's just too verbose for chaining more than once or twice. Factoring into a function frequently doesn't make sense when you just want to do it locally for this one value. And (|| Ok(a?.b?.c?))() always looks like a hack to me, or a way to work around a language feature that doesn't exist yet.

try { a?.b?.c? } is a bit more verbose than a!.b!.b! (or another equivalent syntax), but not by much and it is also more powerful, as @scottmcm mentioned, so I quite like it. IMO, try is the answer.

truppelito avatar Jul 11 '22 09:07 truppelito

Whatever you want, don't use the ! character for more things please. !matches!() is terrible enough.

SOF3 avatar Jul 12 '22 11:07 SOF3

note that match-as-option-and-project (value!.field) and match-as-option-and-call (value!.method()) have very different semantics from the current ? followed by .field/.method() syntax.

struct A { b: B }
struct B {
    c: Option<i8>,
    d: i8,
}

fn stable(a: Option<A>) -> Option<i8> {
    a?.b.c
}
fn stable2(a: Option<A>) -> Option<i8> {
    Some(a?.b.d)
}

fn proposed(a: Option<A>) -> Option<i8> {
    a!.b!.c
}

fn proposed2(a: Option<A>) -> Option<i8> {
    Some(a!.b!.d)
}

It is also ambiguous whether value!.method() is equivalent to value.and_then(method) (FnOnce(T) -> Option<U>) or value.map(method) (FnOnce(T) -> U).

SOF3 avatar Jul 12 '22 11:07 SOF3

@SOF3

Whatever you want, don't use the ! character for more things please. !matches!() is terrible enough.

I agree that ! is not the ideal syntax. I'm hoping the negative response to this idea is more to do with using ! and less about the idea itself.

It is also ambiguous whether value!.method() is equivalent to value.and_then(method) (FnOnce(T) -> Option<U>) or value.map(method) (FnOnce(T) -> U).

! just unwraps the Option<T> or Result<T, E> if it's in the chain. T will always be whatever the end of the chain is, but the end of the chain will always be an Option<T> or Result<T, E>.

#[derive(Default)]
struct Foo {
	bar: Option<Bar>
}

struct Bar {
	baz: Option<Baz>
}

struct Baz;

impl Baz {
	fn do_baz() -> bool {
		true
	}

	fn do_baz_maybe() -> Option<bool> {
		Some(true)
	}
}

let foo = Foo::default();

let data = foo!.bar!.baz!.do_baz(); // Option<bool> 
let data = foo!.bar!.baz!.do_baz()!; // Error, chain end does not produce an `Option<T>`
let data = foo!.bar!.baz!.do_baz_maybe(); // Option<Option<bool>>
let data = foo!.bar!.baz!.do_baz_maybe()!; // Option<bool>

semirix avatar Jul 18 '22 05:07 semirix

@SOF3

Whatever you want, don't use the ! character for more things please. !matches!() is terrible enough.

I agree that ! is not the ideal syntax. I'm hoping the negative response to this idea is more to do with using ! and less about the idea itself.

It is also ambiguous whether value!.method() is equivalent to value.and_then(method) (FnOnce(T) -> Option<U>) or value.map(method) (FnOnce(T) -> U).

! just unwraps the Option<T> or Result<T, E> if it's in the chain. T will always be whatever the end of the chain is, but the end of the chain will always be an Option<T> or Result<T, E>.

#[derive(Default)]
struct Foo {
	bar: Option<Bar>
}

struct Bar {
	baz: Option<Baz>
}

struct Baz;

impl Baz {
	fn do_baz() -> bool {
		true
	}

	fn do_baz_maybe() -> Option<bool> {
		Some(true)
	}
}

let foo = Foo::default();

let data = foo!.bar!.baz!.do_baz(); // Option<bool> 
let data = foo!.bar!.baz!.do_baz()!; // Error, chain end does not produce an `Option<T>`
let data = foo!.bar!.baz!.do_baz_maybe(); // Option<Option<bool>>
let data = foo!.bar!.baz!.do_baz_maybe()!; // Option<bool>

I don't understand why it is foo!.bar! instead of foo.bar!. Can you define precisely if foo! is one expression or if !. is one operator, or is there a third way of reading this?

SOF3 avatar Jul 18 '22 05:07 SOF3

@SOF3

I don't understand why it is foo!.bar! instead of foo.bar!. Can you define precisely if foo! is one expression or if !. is one operator, or is there a third way of reading this?

I think you have a good point. Having one syntax item is a much better idea. Going forward, taking this idea more seriously, it would make more sense to make it a shorthand for the try syntax. But maybe not ! perhaps ?? would make more sense given what it does. So something like:

let foo = foo.bar.baz.do_baz_maybe()??;

Would be equivalent to:

let foo = try { foo?.bar?.baz?.do_baz_maybe()? };

I have no idea how feasible/reasonable this is but this does seem like a much better solution than I originally put forward.

semirix avatar Jul 18 '22 05:07 semirix

But ?? is already legal and means something else:

let x: Result<Result<T, EIN> EOUT> = todo!();
let y: T = x??;

Lokathor avatar Jul 18 '22 05:07 Lokathor

@SOF3

I don't understand why it is foo!.bar! instead of foo.bar!. Can you define precisely if foo! is one expression or if !. is one operator, or is there a third way of reading this?

I think you have a good point. Having one syntax item is a much better idea. Going forward, taking this idea more seriously, it would make more sense to make it a shorthand for the try syntax. But maybe not ! perhaps ?? would make more sense given what it does. So something like:

let foo = foo.bar.baz.do_baz_maybe()??;

Would be equivalent to:

let foo = try { foo?.bar?.baz?.do_baz_maybe()? };

I have no idea how feasible/reasonable this is but this does seem like a much better solution than I originally put forward.

I don't think the notion of "chain" exists in Rust. How would that work with subexpressions? what about (foo.bar).baz?? or {foo.bar}.baz??? how about if bool { foo.bar } else { corge }.baz???

SOF3 avatar Jul 18 '22 06:07 SOF3

@Lokathor Ah, my bad. I forgot about nested results. Whatever the syntax ends up being, having it positioned at the end is probably the best idea.

@SOF3

I don't think the notion of "chain" exists in Rust. How would that work with subexpressions? what about (foo.bar).baz?? or {foo.bar}.baz??? how about if bool { foo.bar } else { corge }.baz???

"Chain" was more or less a way of describing the nature of the expression. With regards to sub-expressions, I think perhaps they are out of scope for this syntax? The goal is to be able to do let nested = really.deep.expression.with.many.options??; in a concise manner.

It is my personal opinion that let nested = try { really?.deep?.expression?.with?.many?.options? }; is fine. Had I known about the try syntax prior to posting this RFC I would have refrained. Unless more people care deeply about having a smaller shorthand this issue can probably be closed.

semirix avatar Jul 18 '22 06:07 semirix