Poll: try! syntax changes

I have been an avid Rust user for a few months now and on the whole I am loving the language. However I am beginning to find that as I move onto more complicated projects the syntax becomes rather verbose and unreadable. Particularly I find that error handling can introduce unneccesary complexity into my algorithms.

The try! macro is clearer raw match expressions, but it is still to verbose (in my opinion) and makes Rust a very dense language to read.

I'm not suggesting we discard all semicolons, braces, parentheses and understandability. I'm sure many people have suggested that before and met harsh opposition and coming from the C-family of languages I'm not sure I like it either.

I belive a try keyword, that has the same function as try!() but doesn't need the parentheses would make a lot of code clearer to understand.

Take this code example, taken from my SPIR-V to GLSL converter.

if try!(self.get_reference(op.result)) != "param" {
    let ty = try!(self.get_type(variable.ty));
    try!(write!(out, "{}{};", self.indent.begin_line(), try!(self.declare_variable(ty, try!(self.get_reference(op.result))))))
}

That verbose monstrosity would become significantly cleaner imo:

if try self.get_reference(op.result) != "param" {
    let ty = try self.get_type(variable.ty);
    try write!(out, "{}{};", self.indent.begin_line(), try self.declare_variable(ty, try self.get_reference(op.result)))
}

In particular, it reduces the six closing parentheses )))))) down to just three ))).

My question is, how many Rustaceans would like this change? I know that many people want Rust to stay really explicit (after all it is a systems programming language not CoffeeScript), however perhaps there is room for some 'streamlining' of the syntax.

It would be great if you could fill out this 2-second poll to let me know what the general consensus is.


Alternatively, I though about supporting the general case and allowing macros to be invoked without delimiters like () or {}, but I'm not sure it that is actually possible to implement at the moment.

The current thinking is to use ? as a postfix operator for this purpose (e.g. this proposal),

if self.get_reference(op.result)? != "param" {
    let ty = self.get_type(variable.ty)?;
    write!(out, "{}{};", self.indent.begin_line(), self.declare_variable(ty, self.get_reference(op.result)?)?)?
}
1 Like

Ah yes I remember reading about that proposal a while ago. Now that you've reminded me it seems like that should be the way to go.

However when I first saw it, I though it would be equivalent to Result::and_then() or Option::and_then(). Although perhaps that's just me thinking of Optional Chaining in Swift and (I think) other languages.

let foo = bar()?.qux()?.baz()?

i.e.: the above would be like:

let foo = bar().and_then(|b| b.qux()).and_then(|c| c.baz())

Instead of:

let foo = try!(try!(try!(bar()).qux()).baz())

Just my two cents.

1 Like

Personally, I want general method-position macro invocation. Then we could use expr.try!(), among other things. ? runs the risk of being hyper-specific syntax that only helps one particular pain point.

2 Likes

Method-position macro invocation would be nice to have, but I feel that something as important as error handling should deserve its own syntax shortcuts.

That way I see it:

  • expr.try!() is 1 character longer than try!(expr)
  • But it can be useful when chaining, eg: expr.try!().bar().try!().baz().try!()

The method chaining argument is a good point that I realised my (not so well thought out) proposal try EXPR doesn't have.

Why, when you can do something that also covers other use cases with the exact same mechanics? One thing I love about Rust is that instead of taking the easy, obvious way forward (new syntax for individual use-cases), it instead tries to add features that have broad use.

Really, I don't think there's anything fundamentally wrong with try! except for the fact that it interacts poorly with chaining (which is so prevalent in Rust). Too much nesting in this case is, I feel, more a reflection of trying to pack too much complexity into a single expression.

6 Likes

Haha yes my try!(write!(out, "{}{};", self.indent.begin_line(), try!(self.declare_variable(ty, try!(self.get_reference(op.result)))))) is definitely rather complex. However less invasive syntax can turn a nightmare expression into something a bit more palatable.

Idea that I have put exactly 0 minutes of thought into: since you can only use try! in a method that returns Result, why not automatically try! any method called within that method that returns Result, unless you choose to handle it explicitly?

Clearly this goes against the Rust preference for explicitness, buuuuut....

That makes for very surprising behavior in medium complex situations, like calling such a method within an iterator loop, effectively introducing a break as a side-effect.

Alternative code paths for the "normal" case and an "error" or "exceptional" case are such a fundamental part of how people think about and write code, that I think having dedicated syntax for it makes a certain amount of sense.

Features with broad use can make sense, but sometimes when using very generic features they will be more heavyweight and cumbersome than dedicated syntax. While let foo = bar.try!().qux.try!().baz.try!() is moderately more friendly than let foo = try!(try!(try!(bar()).qux()).baz()), it is still cumbersome to type and read than let foo = bar()?.qux()?.baz()?.

Now, I do wonder whether using ? as Optional-chaining, like Swift, would be better than using it as sugar for try!(). If you treated it as something that could chain Option or Result types (probably using From to convert Err results to a common type like try!() does), then you could use it within expressions and use try!() just once for the control-flow effect (which is a little more obvious when called out in a macro):

let foo = try!(bar()?.qux()?.baz()?)

Or if you don't want magic control flow:

match bar()?.qux()?.baz()? {
    Ok(x) => ...,
    Err(BarErr(e)) => ...,
    Err(QuxErr(e, errno)) => ...,
    _ => ...,
}

I've read through most of the trait-based exception handling RFC and thread, and don't see something like this proposed, though it seems much simpler than the existing code. Has anyone considered a syntax like this?

Usage of ? as and_then() sugar is limited to things you can directly call via method syntax. For example, this taken directly from the book error handling would get no help whatsoever from such a syntax. ? as a try! replacement does seem strictly more general.

What's good about addressing only this very specific (and ubiquitous) pain point, is that it has lower risk of bad interaction with other language features. Just a bit of syntactic sugar: expr? => try!(expr).

I am a bit scared of a tendency to consider a solution of a particular problem inferior to a bit uglier solution of entire class of maybe-not-yet-encountered problems (sometimes, iostream nightmares wake me up (just kidding))

As a solution for this particular case, I'd consider allowing macro invocation without parens if macro accepts only single expression:

try!(foo(try!(bar(try!(baz())))))
// transforms into
try! foo(try! bar(try! baz() ) )

But there's a much more generic and appealing solution - monads and do-notation:

do! {
    a <- baz()
    b <- bar(a)
    c <- foo(b)
} 

Or even function chaining:

chain! { baz() >>= bar() >>= foo() }

The problem is, both require HKT's in some form, and the second one possibly requires variadic tuples manipulation to support currying.
To sum up, I'd prefer monadic do-notation as a more generic solution.

4 Likes

Sometimes I think I'm the only one who loves try!. Stuffing too many of them on a single line can make for noisy code, and the answer to that IMO is to break that line into multiple lines. It almost always leads to clearer code.

If it weren't for the fact that ? is so darn useful during chaining, I'd probably think it wasn't worth adding.

7 Likes

Nope, I do too!

6 Likes

try! is a really good anti-boilerplate solution in the world of result codes.
At least, it saves us from Golang's repetitive if err != nil { ... }.
But what I'd like to see is dropping those parens, for single-expression macro. If it's possible, of course.

1 Like

Both more than one try!() per line, just like more than one of the proposed ? just makes for very dense code to read. I think you can improve it just by adjusting the style.

Ah. Yep. That would be bad :smile:

1 Like

I was going to say the same. To me the correct solution would be to remove the chaining as that's bad design anyway.

That's the first thought came to me when I read the OP. Seems like you pinned it earlier than me, so all I can do is to second you on this point.