Qqwy

Qqwy

TypeCheck Core Team

A simple macro to allow bare captures in pipes

EDIT: CapturePipe has been released as a library!
Please give it a go and let us know if you find any mistakes.


Hi everyone!
Yesterday evening (it was already late… :sleeping:) I was reflecting a bit about common patterns in my Elixir code.
If you’re anything like me, you’ll often end up with functions that look like this:

def some_function(argument, other_argument) do
  changed_argument = 
    argument
    |> other_function(other_argument)
    |> more_work(42)

  {:ok, changed_argument}
end

This type of code is highly prevalent when working with ok/error -tuples, but also when working with GenServers or anything based on them (like e.g. Phoenix Channels or Phoenix LiveView handlers) because the callbacks of these behaviours require you to return tuples all the time as well. An example is:

def handle_event("reset-button", _params, socket) do
  new_socket = 
    socket
    |> assign(:form_values, default_form_values())
    |> assign(:error_messages, [])
    |> assign(:reset_button_active, false)

  {:noreply, new_socket}
end

This inserting into tuples (and sometimes other datastructures) is in tension with working with pipelines. It requires ‘breaking the pipeline’. Flow no longer goes left-to-right, top-to-bottom but rather back to the top (where some new variable is bound) and then continues below the pipeline.

Yesterday evening I realized that there was a tiny sprinkle of syntactical sugar that would resolve this tension:

defmodule Capturepipe do
  @doc """
  A pipe-operator that extends the normal pipe
  in one tiny way:

  It allows the syntax of having a bare `&1` capture
  to exist inside a datastructure as one of the pipe results.
  This is useful to insert the pipe's results into a datastructure
  such as a tuple.

  What this pipe-macro does, is if it encounters a bare `&1` capture,
  it wraps the whole operand in `(&(...)).()` which is the
  anonymous-function-call syntax that the Kernel pipe accepts,
  that (argubably) is much less easy on the eyes.

  So `10 |> {:ok, &1}` is turned into `10 |> (&({:ok, &1})).()`


  To use this operator in one of your modules, you need to add the following to it:
  
      import Capturepipe
      import Kernel, except: [|>: 2]
  

  ## Examples

  Still works as normal:

  iex> [1,2,3] |> Enum.map(fn x -> x + 1 end)
  [2,3,4]

  Insert the result of an operation into a tuple

  iex> 42 |> {:ok, &1}
  {:ok, 42}

  It also works multiple times in a row

  iex> 20 |> {:ok, &1} |> [&1, 2, 3]
  [{:ok, 20}, 2, 3]
  """
  defmacro prev |> next do
    # Make sure the pipes are expanded left-to-right (top-to-bottom)
    # to allow consecutive applications of the capturepipe to work
    prev = Macro.expand(prev, __CALLER__)

    # Perform change only if we encounter a `&1` that is not wrapped in a `&(...)`
    {_, visible?} = Macro.postwalk(next, false, &capture_visible?/2)
    if visible? do
      quote do
        Kernel.|>(unquote(prev), (&(unquote(next))).())
    end
    else
      quote do
        Kernel.|>(unquote(prev), unquote(next))
      end
    end
  end

  @doc false
  def capture_visible?(ast = {:&, _, [1]}, _bool), do: {ast, true}
  def capture_visible?(ast = {:&, _, _}, _bool), do: {ast, false}
  def capture_visible?(ast, bool), do: {ast, bool}
end

Also available in this Gist.

This allows us to write above example snippet as

def handle_event("reset-button", _params, socket) do
  socket
  |> assign(:form_values, default_form_values())
  |> assign(:error_messages, [])
  |> assign(:reset_button_active, false)
  |> {:noreply, &1}
end

Now I know that opinions on enhancing the pipe-operator in general are divided.
Personally I think that this tiny bit of sugar is easier to understand (especially for people seeing the code for the first time!) than e.g. defining manual ok(...) or noreply(...) wrapping functions and I think it can be an improvement on the ‘breaking of the pipeline’ that is currently required.

That said, I am not (yet) releasing this snippet as a library, because:

  • even though it is a tiny bit of sugar and macro-code, it might still be somewhat brittle.
  • I’d rather start a bit of discussion about this syntax to hear what other people think about this sleep-deprived idea I had yesterday-evening late before committing to it :grin: .

Your input and feedback is greatly appreciated!

Most Liked

josevalim

josevalim

Creator of Elixir

Today you can write: & foo |> bar(:value, &1) |> baz(). With your proposal, how do you know if &1 binds to the wrapping & or it is just an extension of the pipe? :slight_smile:

henrik

henrik

egze

egze

Another common pattern is to write like this:

def handle_event("reset-button", _params, socket) do
  {:noreply, socket
             |> assign(:form_values, default_form_values())
             |> assign(:error_messages, [])
             |> assign(:reset_button_active, false)}
end

Last Post!

mat-hek

mat-hek

Membrane Core Team

Just FYI I implemented something very similar in Bunch and have been using it for almost two years. Really helpful thing, especially for prototyping :slight_smile:

Where Next?

Popular in Discussions Top

scouten
I’m looking for a host for the server part of a small (personal) side project that I’m working on. It’s currently written in Node.js and ...
New
restack_oslo
Hello, Please pardon me for any faux paux. I am 46 and this is my first time on a forum of any kind. I wanted to to get answers from tho...
New
pillaiindu
In django there is a cache framework backed by memcached. Rails also puts a lot of emphasis on caching, and even the idea of russian-doll...
New
chulkilee
Here are the list of HTTP client libraries/wrappers, and some thoughts on HTTP client in general. I’d like to hear from others how they w...
New
marciol
Please, let me know if this kind of discussion already took place in another topic . Hi all, how do you consider if is better to build ...
New
crispinb
On reading dhh’s latest The One Person Framework it strikes me that Phoenix with LiveView is already pretty much this. However, never hav...
New
matthias_toepp
I’d love to hear what people think about Wisp, the new Gleam web framework started by Gleam’s primary creator Louis Pilfold. Gleam, alon...
New

Other popular topics Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42533 114
New

We're in Beta

About us Mission Statement