Correct. Although, writing a simple attempt to implement it I found out that & will capture everything that follows it, even if parentheses are used, meaning that using Elixir’s current syntax rules
10 |> &({:ok, &1})) |> &([1,2,3, &1]
is interpreted as
10 |> (&{:ok, &1} |> (&[1, 2, 3, &1]).()).()
which will fail because of ‘nested captures’.
rather than the desired
10 |> (&{:ok, &1}).() |> (&[1, 2, 3, &1]).()
Here is an updated implementation that fixes this, by rewriting the construct a |> &b |> c to a |> (&b) |> c where a, b, and c are arbitrary ASTs .
At first I was a bit concerned that this more rigorous rewrite would alter the behaviour of existing programs, but this is not the case since this syntax would not compile without the extension. The extension is thus backwards compatible.
defmodule Example do
import Capturepipe
import Kernel, except: [|>: 2]
def main(greeting, name) do
name
|> &greet(greeting, &1)
|> &{:ok, &1}
end
def greet(greeting, name) do
IO.puts("#{greeting}, #{name}!")
end
end
Maybe you made a mistake with the import statements?
Nope, it does not; the implementation I made does not seem to handle deeply nested pipes correctly. Essentially something like a |> & b |> c |> d which we would want to convert to a |> (& b).() |> c |> d is still converted to a |> (& b |> c |> d).() which poses problems when there are captures in e.g. c or d.
It is definitely possible to fix this, but I have not had the time to get around to it yet.
For who did not keep up with the conversation in the issue+PRs, we realized that an implementation was somewhat non-trivial because of the precedence-difference between the pipe- and capture-operators.
Because of this reason, the issue and related PRs were closed for the time being, and I would attempt to create it as a library for now.
And now I finally found the time to go through with writing this library .
It supports both bare function-captures and bare anonymous functions.
As far as I can tell, the code is fully backwards-compatible with any existing Elixir-code.
In the end, the implementation consists of a slight alteration of Elixir’s builtin Macro.unpipe/1 (which is altered to turn a structure of nested-pipes-in-captures(-in-pipes-in-captures…) inside-out and Macro.pipe which wraps a capture or anonymous function whenever it is encountered.
I have no doubt the code could be improved upon further (for instance: raising compile-time errors when a capture or function with an arity other than 1 is used rather than failing at runtime) but that is embellishment for later. The foundation is solid, and it would be great if people could try it out.
I am especially interested in if there are still situations in which this code might fail to work when added to existing Elixir-code, because the intent is of course that this enhancement to the pipe operator is completely backwards compatible.
iex> list = [1,2,3]
iex> list |> Enum.map(& &1 + 1)
[2, 3, 4]
Captures are only manipulated if they are at the ‘top’ level of a pipe. If they are inside of something else like a function call such as in your example, they will be kept exactly as-is.
Very cool. I pulled it into the majority of the modules in my codebase and all tests are still passing. The only issue I run into is the formatter goes nuts with parentheses and turns something like this…
This is unfortunately something that user-code has very little control over; the Elixir formatter exposes (by design) only very little configurable settings.
Maybe that adding &: 1 to the locals_without_parens helps but probably not (since the expression spans multiple lines).