Is it possible to have a better .expect() function that accept formatted literials?

TL;DR Is there some better syntax to replace the verbose and annoying result.unwrap_or_else(|err|panic!("formatted string with error {err} and more infos"))? If postfix is the solution, is it possible to define postfix macros only on struct/trait objects?


Currently I found it difficult to replace .unwrap() with .expect("Reason") since "Reason" usually contains valuables variable to be formatted.

When I ask how to write .expect with variables formatted, AI told me either using a verbose pattern, such as unwrap_or_else(|_|panic!(...)), or creating a &str with .expect(&format!(...)), which will facing the penalty that each time we called the function, a String will be formatted no matter whether the result is Ok or Err.

It seems that, a postfix macro might helpful, since we could write something like

foo.expect!("Reason")

which could format all the data we need.

I have searched for several posts of postfix macros, most of the worries are talking about "what to expand". If we limit the postfix macros to trait/struct only, then what to expand is obvious. And for macros that will expand a lot of things, perhaps a postfix will confuse users who read it.

5 Likes

Postfix macros that operate on a specific type are much harder than those who work generically, since they require access to type information, something macros currently cannot access, and letting them access will require a major re-architecture of the compiler.

For this reason, the current open RFC on postfix macros only proposed type-agnostic macros.

1 Like

I realize that postfix is often more convenient, but you could always write a normal macro:

expect!(foo.unwrap(), "didn't work :( (bar={bar})");

which at least to me actually reads better than the method expect:

expect(what_i_expect_to_happen, didnt_happen)

rather than

what_i_expect_to_happen.expect(didnt_happen) 

which raises the common question as to how exactly didnt_happen should be phrased to make the most sense.


We do also have a way to represent a lazily formatted string: format_args! and Arguments. If expect could be generalized from taking &str to &dyn Display (or &impl Display) or some more specific trait, that would take care of most of the use cases of the unwrap_or_else(|| panic!(...)) pattern. That said, it's probably not exactly a priority to make it more convenient to panic on errors.

1 Like

I prefer write let Ok(val) = result else { panic!("...") } when applicable. Not usable in chain method call though.

5 Likes

The only issue I have with let-else is that I generally do want the contents of the Err for the panic string, which leads me to have to fall-back to match:

let val = match foo {
    Ok(val) => val,
    Err(err) => panic!("error: {err:?}"),
};

This not terrible, but makes me miss let-else :slight_smile:

2 Likes

Cursed alternative (typed directly into forum):

let Ok(value) = foo.map_err::<Infallible>(|e| panic!("{e}"));

I forget if this exhaustive matching made it into stable yet.

9 Likes

If we were going to do something for this, I think it's this: "else match" Extending Conditional Syntax - #8 by scottmcm

But it would need someone to analyze the frequency and prevalence at which that would both be applicable and not just be .unwrap_or_else(|e| panic!("error: {e:?}")).

3 Likes
let Ok(value) = foo.map_err::<!>(|e| panic!("{e}"));

:red_exclamation_mark:

2 Likes

You don't even need the ::<!>:

let Ok(value) = foo.map_err(|e| panic!("{e}"));

And this compiles on stable!

8 Likes

…but is any of this better than unwrap_or_else? Not really.

7 Likes

If .expect() took impl Display argument, then you could have x.expect(format_args!("{z}")) that wouldn't need to heap-allocate anything upfront.

It's still doable with an extension trait.

8 Likes

Why not create a macro to embody the unwrap_or_else usage pattern. The elimination of such replication is what a prelude of declarative macros is for.

Good luck.

Issues occurs when you have a really long chain. This at least requires a postfix macro.

In case you write a chain, you can understand what happens easily:

some_thing
    .become_another_thing()
    .and_do_some_work()
    .finally_become_a_result()

If you want to write macros, things will be harder:

unwrap!(//what to unwrap?
   some_thing
    .become_another_thing()
    .and_do_some_work()
    .finally_become_a_result()
, panic!("error occurs") //which macro accepts this panic? Am I put the panic! into the wrong place?
).in_case_you_want_to_do_more_things()

There are various ways to write equivlent code, but I’m trying to find a simple one.

My immediate idea is to employ a suitable wrapper function or method such as would be required to invoke a lambda. This treatment could be generalized by a trait.

...
do_something()
.wrapper1(|x| ... panic!(...)...)
.wrapper1(do_code_rewrite!(x))
.do_some_more_thing()
...

If macro aliases were a thing and expect accepted impl Display, user may have aliased format_args! to fa! and used like .expect(fa!(...)). Of course such alias won't make it to the std

1 Like

But... they are a thing!

use format_args as fa;

fn main() {
    fa!("abc {}", 123);
}
6 Likes

This makes me think that the correct solution here is to add a new method that's a hybrid of map_err and into_ok:

let value = foo.into_ok_or(|e| panic!("{e}"));

Basically unwrap_or_else but where the closure's return type is Into<!> rather than T. I don't tihnk this does anything that unwrap_or_else can't do, but it's more readable.

1 Like

This already works today. I can't think of a realistic expression that returns Into<!> instead of !. So I'm not sure where the "it's more readable" comes from.

let value = res.unwrap_or_else(|e| panic!("{e}"));
1 Like

The name: into_ok_or is a much clearer name for what's going on than unwrap_or_else (which normally expects the closure to return a value).