Option<T> / Result<T, E> rewrap shorthand
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.
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.
No need for new syntax, the following works:
fn get_c(a: Option<A>) -> Option<C> {
a?.b?.c
}
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 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.
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.
+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.
Whatever you want, don't use the ! character for more things please. !matches!() is terrible enough.
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
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 tovalue.and_then(method)(FnOnce(T) -> Option<U>) orvalue.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>
@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 tovalue.and_then(method)(FnOnce(T) -> Option<U>) orvalue.map(method)(FnOnce(T) -> U).
!just unwraps theOption<T>orResult<T, E>if it's in the chain.Twill always be whatever the end of the chain is, but the end of the chain will always be anOption<T>orResult<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
I don't understand why it is
foo!.bar!instead offoo.bar!. Can you define precisely iffoo!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.
But ?? is already legal and means something else:
let x: Result<Result<T, EIN> EOUT> = todo!();
let y: T = x??;
@SOF3
I don't understand why it is
foo!.bar!instead offoo.bar!. Can you define precisely iffoo!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
trysyntax. 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???
@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 aboutif 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.