Chaining Computations

The Elixir language allows for chaining computations using its |> operator, which inserts the left-hand argument as the first argument of the function call on the right-hand argument:

iex> "Elixir" |> String.graphemes() |> Enum.frequencies()
%{"E" => 1, "i" => 2, "l" => 1, "r" => 1, "x" => 1}

We would like to achieve the same thing in Rust using a visitor. Since we want to use a visitor but using syn builtin parsing capabilities, we will take a valid Rust tree as input. Therefore, we will sacrifice the | character (pipe, which represents "bitwise or") for this purpose.

#[pipe]
fn pipe_example() {
  let f = "Rust" | str::chars | Vec::from_iter;
  assert_eq!(f, vec!['R', 'u', 's', 't']);
}

This code is equivalent to:

fn pipe_example() {
  let f = Vec::from_iter(str::chars("Rust"));
  assert_eq!(f, vec!['R', 'u', 's', 't']);
}

Our #[pipe] macro and its | operator should also support function calls with multiple arguments. The expression a | f(b, c) is equivalent to f(a, b, c).

Implementation

Exercise 6.a: Use a syn::visit_mut::VisitMut visitor to intercept the analysis of expressions and replace a binary expression using the | operator followed by a function call or a path with a function call that uses the left-hand argument as the first argument of the call. Remember to recurse to visit the new node (or the sub-nodes if no transformation was made).

Consider what type of node you want to visit in order to modify it. For example, if you are modifying a ExprBinary node that represents a binary expression, you cannot replace it with a node that is not a binary expression. If you are modifying an Expr node that is less specific, it allows you to replace an expression with another.

Also, remember that you can use syn::parse_quote!() to generate nodes of any type from a template.

Exercise 6.b: Create a procedural macro attribute pipe that uses this visitor to transform any item using the visitor.

Exercise 6.c: Add tests to verify the correct behavior of this macro, which should be applicable to functions, modules, and implementations.