The ? operator will be harmful to Rust

The point is that for normal methods, the code executed will depend on the type of the variable, and the Deref trait further complicates this. So if there is such a sugar for macros, it’s inconsistent with the method call syntax. (Discourse, let me post this, the body is similar to the post I posted, but I deleted it already!)

Hmm, not sure if I understand your point. bla.foo!(x) is syntax sugar for foo!(bla, x). From that, you can just treat &deref_something.foo!(expr) as foo!(&deref_something, expr). Macros treat Derefs as they are values passed to macros.

The way it's written it's actually quite consistent with method call syntax, since all method call syntax (e.g. el.foo(param)) is sugar for foo(el, param).

The method call syntax el.foo(param) is sugar for typeof(el)::foo(el, param), or actually <T where typeof(el) as Deref<Target=T>>::foo(el.deref(), param). So what foo() is, depends on the type of el. Macros can’t depend on type resolution.

In my example they don't depend on type resolution?

Method call macro as presented can be called on any expression E.g.

 #[rust_macro_method]
 pub fn foo(self: expr, input: TokenStream) -> TokenStream { 
    //code goes here...
 }

And you call it like 2.foo!(blagh). It could be completely pointless, or cause an error but that's what macros do, no?

I think the key point you’re missing is this: “method macros” look the same as regular methods, so they should behave in broadly the same fashion. It would be entirely reasonable for someone to assume that subject.method!() means the method! macro is somehow tied to the type of subject, just like it would be with subject.method(). This is not true, and will confuse people.

That method macros could be applied to any type at all is the problem.

(Disclaimer: I’m favour of method position macro invocation. Still have the pre-RFC kicking around somewhere…)

3 Likes

@DanielKeep: How do method position macro work in your pre-RFC? Trait based? I’m genuinely curious.

I do see your point about subject.method looking a bit like subject.method!, but for me the exclamation mark is enough to show something weird is happening. I wonder if there is a way to test how people react to that kind of syntax.

You can’t do it with traits: macros are processed before those exist. There were a few alternatives, but the one I liked was actually making them require distinct rules to normal macros. So something akin to:

macro_rules! try_suffix { $self.() => { try!($self) } }

But that’s getting off-topic.

The main argument boils down to whether try!(...) is more or less readable than ...? in a way that impacts code quality.

Does having two forms for the same thing lead to more errors? Do code reviewers catch more errors when code uses the try! syntax or the ? syntax? These aren't questions we have answers to. This is what I meant when I said that the decision wasn't based on clear evidence.

In fact, there might not even be a uniform answer to these questions. For example, security/crypto code is in many ways mostly error checking. In this class of code, try! might be better because the presence or absence of specific error checks is a core concern, as is even the precise location relative to other code where a check is done. In this kind of code, doing fancy things with function chaining or any kind of complex expression or statement is considered bad, so ? doesn't offer much benefit.

But, for GUI code or database code, maybe it mostly just matters that errors are handled "somehow" in which case the "out of the way" nature of ?, and the way it helps build complicated chained expressions, may be better.

1 Like

This is RFC issue 676. But macro resolutions should be done before type checking. I don’t think “binding a macro to a type” will ever work.

Did you try it? Because the argument about not seeing it or forgetting it I cannot really understand because it's a compile error to not use it. I would love to see a code example where try! is clearer than ? and how the latter causes issues.

3 Likes

That might be well true. Do you happen to have a small self-contained example of such crypto code that uses try! ? We could then rewrite it with ?, put them next to each other, and try to figure out things from there.

1 Like

It could be nice to have consistent notation for results and futures, so that you can switch back and forth with minimal effort. But right now futures-rs and tokio are still being developed, so perhaps it is too soon to make a decission. Therefore I suggest waiting a few months before stabilizing the ?-operator.

1 Like

Three links to three github repos are not particularly a self contained example of anything...

Anyhow, I rewrote untrusted and ring to use ?, and it took 10 minutes to do so (for both). The only error handling that happens is of the form:

let a = some_fn()?;
if some_cnd {
  a
} else {
  Err(b)
}

or

let a = some_fn()?;
let b = some_other_fn()?;
if some_cnd {
  (a, b)
} else {
  Err(c)
}

But basically the following describes the error handling of those projects:

  • there is very little error handling code (almost zero compared to non-error handling code),
  • there is no chaining of errors (none at all),
  • there are no fluent/functional APIs being used. Instead the pattern: "try something, perform some side-effecting operation on something result, check side effects, return based on side effects" is used a lot (probably for performance).

Whether ? reads better than try! in these cases is obviously subjective, but arguably, for the little amount of error handling code that there is, if you really want to be explicit, you don't even need to use try!. And if you are willing to use try!, replacing it with ? doesn't make things worse (it might just require one getting used to it, but if you want to keep using try! I think that is fine too). The only change is:

let a = try!(foo(b));

becomes

let a = foo(b)?;

But that's basically it, these projects don't do any "more complicated" error handling.

This is why I think that in the particular field / style that these crates are written, whether ? or try! or try ... catch is accepted is kind of irrelevant, since the crates do so little error handling that whatever solution is stabilized, they will be barely affected by it (if at all).

It is worth remembering that Rust is used in many different fields. Some of them do no error handling at all, and some of them do a lot of error handling, chaining errors all the time. If you work in a field where almost no error handling is required, and when it is required, a simple try!(foo(a)) suffices, then obviously you won't see any benefit in adding ?.

This doesn't mean that ? doesn't add any value to Rust, but rather that it doesn't add any value to the particular field in which you are using Rust. Systems programming languages are used in so many fields that it is impossible to know what the majority of programmers are actually doing. One of Bjarne's famous quotes is that nobody knows what most C++ programmers do. The same can be said about Rust.

5 Likes

Yep. Again, I'm no longer arguing against the ? feature. I'm merely trying to help people understand why there may be such a bimodal distribution of opinions on this particular feature, so that people on one side of the debate can understand the other side a little better.

Anyway, thanks for giving it a try!

(Incidentally, "The Dangers of Error Checking" in Ideas for a New Elliptic Curve Cryptography Library helps explain why my code has its error checking the way it is, in case you were curious.)

2 Likes

For what it’s worth I could imagine that something like #[deny(result_carrier)] could be useful for highly specific code that wants to be explicit about error handling and that would prevent both ? and try! to be used in the annotated section.

1 Like

For writing code, yes, it's convenient. For reading a bit less. I already had a few cases where I initially didn't recognize the error handling and thought it was missing. OTOH I'm accustomed to try! much more than to ?.

1 Like

If anything I would argue that's an advantage because the result propagation should not be the first thing you notice. I have looked at a lot of code now that deals with early returns and it does not hide anything of significance in safe code. It's a different story in unsafe code where something might start leaking memory because stuff is not properly guarded.

1 Like

But I was explicitly looking for the error handling. I was mostly cases like this:

let x: T = foo(..)?;
// do something with x

Where I thought "Wait, this doesn't work if x == someInvalidValue" and added some error handling. Only later to discover that the case was already handled in foo. The reason for this is that I thought that foo returns a T, when in fact it returned a Result<T,..>.

My preferred solution would be a combination of ? that does result chaining instead of early return and then a "method-style" macro for early returns:

let x = foo()?.bar()?.filter().check!();

That would combine several advantages:

  • ? operator similar/equal to other languages
  • concise pipeline-style functional code
  • easy recognizable "early return points"
  • no try!(...)-"blocks" spanning several lines.
1 Like