Announcing if_chain
Today I published if_chain
, a macro for writing nested if let
expressions. The documentation does a good job of showing how to use the crate, and I recommend taking a look through it. This article will instead go more into the background behind this macro and how it helped with my own projects.
The problem
As part of another project, I was working on a lint plugin that catches common mistakes and suggests ways to fix them. Unfortunately, the code I wrote would often look like this:
if let ExprCall(ref path_expr, ref args) = expr.node {
if let Some(first_arg) = args.first() {
if let ExprLit(ref lit) = first_arg.node {
if let LitKind::Str(s, _) = lit.node {
if s.as_str().eq_ignore_ascii_case("<!doctype html>") {
// ...
As you can see, a common issue was rightward drift. Every if
statement would indent the code by one more step, such that the actual message ended up off the page!
Now, Rust does provide tools for tackling this issue; and in most cases it would be enough to use them. But for my use case—writing lints—they are not enough:
-
We can rewrite each check to yield an Option
, and use .and_then()
or the ?
operator to chain them. But when writing a lint, the interfaces involved are so broad and irregular that wrapping everything is not practical.
-
We can try to merge all these checks into a single pattern. But in this case, the intermediate nodes are wrapped in smart pointers (the P
type), and current Rust doesn’t have a way to dereference a smart pointer from within a pattern.
Existing solutions
I wasn’t the first to run into this problem. Clippy, a collection of general-purpose lints, has a utility macro called if_let_chain!
for this purpose. Using this macro, the example above would be written like this instead:
if_let_chain! {[
let ExprCall(ref path_expr, ref args) = expr.node,
let Some(first_arg) = args.first(),
let ExprLit(ref lit) = first_arg.node,
let LitKind::Str(s, _) = lit.node,
s.as_str().eq_ignore_ascii_case("<!doctype html>"),
// ...
], {
// ...
}}
This solved the rightward drift problem at hand. But as I used the macro, I found a few flaws in the implementation:
-
Since if_let_chain!
is a part of Clippy, I would have to either copy-and-paste the macro, or depend on the whole of Clippy. It would be better if the macro was in its own crate.
-
When inspecting the type of an expression, for example, the code involved can be quite long. One would use intermediate variables (let
statements) to keep the code easy to read. But since if_let_chain!
expects every line to be an if
or if let
, there’s no good way of doing this refactoring.
-
Some of the syntax choices, like omitting the if
from each check and the use of square brackets, seem arbitrary to me. I’d prefer it if the macro looks more like the generated code.
Introducing if_chain
Here’s where if_chain
comes in. It addresses the points raised above, and adds some features of its own:
-
Fallback values. if_chain!
lets you give an else
clause, which is evaluated when any of the checks fail to match.
-
Multiple patterns. Rust allows for matching multiple patterns at once in a match
expression. For example, this code:
let x = 1;
match x {
1 | 2 => println!("one or two"),
_ => println!("something else"),
}
prints “one or two.” if_chain!
supports this syntax in if let
as well.
Our example now looks like this:
if_chain! {
if let ExprCall(ref path_expr, ref args) = expr.node;
if let Some(first_arg) = args.first();
if let ExprLit(ref lit) = first_arg.node;
if let LitKind::Str(s, _) = lit.node;
if s.as_str().eq_ignore_ascii_case("<!doctype html>");
// ...
then {
// ...
}
}
Delicious!