6 releases

0.2.0 Apr 12, 2020
0.1.5 Aug 20, 2016
0.1.3 Apr 11, 2016
0.1.2 Feb 29, 2016
0.1.0 Dec 7, 2015

#1052 in Procedural macros

MPL-2.0 license

8KB
64 lines

Try-Let

Latest Version Documentation Build Status

This is an implementation of a try-let similar to the one proposed in RFC #1303, as a proc macro.

NOTE: Proc macros in statement position are currently unstable, meaning this macro will not work on stable rust until PR #68717 is merged.

See the documentation for more details.


lib.rs:

This is an implementation of a try-let similar to the one proposed in RFC #1303, as a proc macro.

NOTE: Proc macros in statement position are currently unstable, meaning this macro will not work on stable rust until PR #68717 is merged.

Usage

try-let is implemented using a proc macro instead, as parsing the pattern expression in the way which try-let needs to is not possible with a macro_rules! macro.

This plugin currently requires enabling #![feature(proc_macro_hygiene)], like so:

#![feature(proc_macro_hygiene)]
use try_let::try_let;

The actual use is fairly similar to a let expression:

try_let!(Some(x) = foo else {
    return Err("Shoot! There was a problem!")
});

The expression after else must diverge (e.g. via return, continue, break or panic!).

This also handles more complex types than Some and None:

enum E {
    A(i32, i32, i32, i32, Option<i32>, Result<(), i32>),
    B,
}

let foo = E::A(0, 21, 10, 34, Some(5), Err(32));

try_let!(E::A(a, 21, c, 34, Some(e), Err(f)) = foo else {
    unreachable!()
});
// a, c, e, and f are all bound here.
assert_eq!(a, 0);
assert_eq!(c, 10);
assert_eq!(e, 5);
assert_eq!(f, 32);

Why

This provides a simple way to avoid the rightward-shift of logic which performs a large number of faillible pattern matches in rust. This allows the main logic flow to continue without increasing the indent level, while handling errors with diverging logic.

How

a try_let!() invocation expands to the following:

try_let!(Some(x) = foo else {
    return Err("Shoot! There was a problem!");
});
// ... becomes ...
let (x,) = match foo {
    Some(x) => (x,),
    _ => {
        return Err("Shoot! There was a problem!");
    }
};

A note on None and empty enum variants

A question which some people will be asking now is how are enum variants like None handled?

try_let!(None = foo else {
    return;
});
// ... becomes ...
let () = match foo {
    None => (),
    _ => {
        return;
    }
};

None isn't mistaken for a binding variable by try-let because of the dirty little trick which try-let uses to function: which is that it is powered by rust's style conventions. There is no way for the parser (which is all that the syntax extension has access to) to determine whether a lone identifier in a pattern is an empty enum variant like None or a variable binding like x. This is determined later in the compiler, too late for this extension to use that information.

Instead, the extension checks the first character of the identifier. If it is an ASCII capital, we assume it is a empty enum variant, and otherwise we assume it is a variable binding.

Dependencies

~1.5MB
~37K SLoC