Yesterday I was answering this SO question, roughly asking for how to use an uncommon syntax in pipe operator.
“Man, that’s Elixir,” was my very first response. “We have macros for that.”
The pitfall here is: while we can restrict exporting the standard
Kernel.|>/2
and overload it,
this approach does not sound as a good proposal since we’ll lose all the nifty
default functionality. Of course, one might check the right operand and if it’s
one of those expected by standard pipe, fallback to default Kernel.|>(right)
,
but this is not nifty at all.
So, I grepped Elixir source code for “pipeline” keyword and found this:
@pipeline_operators [:|>, :~>>, :<<~, :~>, :<~, :<~>, :<|>]
I was unable to duckduckgo anything related to what are permitted pipe operators in Elixir, and I even am not sure this behaviour being an implementation detail or is it guaranteed to remain the same, but I wrote some checks and all those are working pretty fine as pipe operators in all the modern versions of Elixir I have on hand.
I am going to show how to use the custom pipe operator in your codebase to make the code looking sexier, in a case you have a colleague to impress. Let’s stay with the question as it was stated on SO:
There is a need to update the struct without breaking the pipeline up. Something like this:
my_struct |> %{ | my_field_in_struct: a_new_value} |> my_funct1 |> %{ | my_field_in_struct: a_new_value} |> my_funct2 |> %{ | my_field_in_struct: a_new_value} |> my_funct3
Let’s start with introducing our own pipe operator implementation:
defmodule StructPiper do
defmacro __using__(_) do
quote do
require unquote(__MODULE__)
import unquote(__MODULE__)
end
end
# TODO: implement this
defmacro left ~>> right do
IO.inspect {left, right}, label: "DEBUG"
:ok
end
end
The code above might be used like this:
defmodule MyStruct do
defstruct ~w|foo bar baz|a
end
defmodule StructPiper.Test do
use StructPiper
def test do
%MyStruct{foo: 42}
|> IO.inspect(label: "1")
~>> [bar: 3.14]
|> IO.inspect(label: "2")
~>> [baz: "FOOBAR"]
end
end
IO.inspect(StructPiper.Test.test ==
%MyStruct{bar: 3.14, baz: "FOOBAR", foo: 42}, label: "Test")
Let’s run the test to check what do we have there:
iex|1 ▶ m = %MyStruct{foo: 42}
iex|2 ▶ m ~>> [bar: 3.14]
#⇒ DEBUG: {{:m, [line: 24], nil}, [bar: 3.14]}
OK, we receive left
and right
operands as expected. The only thing we need
would be to transform them to the AST for %{left | right}
. Let’s check
how the latter looks like:
iex|3 ▶ quote do: %{m | bar: 3.14}
{:%{}, [], [{:|, [], [{:m, [], Elixir}, [bar: 3.14]]}]}
Check the last part of the AST related to :|
operator in this map/struct
updating syntax: arguments are exactly the same as we got in the call to
our pipe macro. That said, in this particular case it would be easier not to
tackle with quoting/unquoting and return the AST out of the box:
defmacro left ~>> right do
{:%{}, [], [{:|, [], [left, right]}]}
end
We’re all set! Below is the full working implementation of the pipe operator
macro :~>>
:
defmodule StructPiper do
defmacro __using__(_) do
quote do
require unquote(__MODULE__)
import unquote(__MODULE__)
end
end
defmacro left ~>> right do
{:%{}, [], [{:|, [], [left, right]}]}
end
end
If you don’t like this particular pipe operator, feel free to pick another from the list this post starts with.