Qqwy
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…
) 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
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
.
Your input and feedback is greatly appreciated!
Trending in Discussions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #hex
- #security











Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
henrik
It’s a cool idea!
A simpler but less general way to address this could be to define private functions to let you do
or
The names could be improved, and I’ll leave the implementation as an exercise for the reader
(EDIT: Sloppy reading on my part – you mentioned that already.)
For stuff like LiveView, one could also define e.g. a handle_event to be shared by all LiveViews, inside MyAppWeb, and have it call my_handle_event and then wrap it - though personally I think I wouldn’t go that far. There’s something to be said for sticking to the conventions, slight boilerplate be damned
ityonemo
baldwindavid
I’ve seen so many new syntax suggestions to avoid breaking the pipe. For the most part, it has always seemed unecessary. You can always create a separate function or write it inline (albeit verbose and ugly). It’s not that I don’t want an easier way to do it inline, but nothing I’ve seen has really appealed to me.
But I really love this. If I saw it in a codebase, I would immediately understand its intent because it is just a shorter version of the existing. It enhances the flow and readability of a function without introducing a new operator.
I look forward to others poking holes in this, but my initial reaction is that this is the exact elegant syntax I’ve always wanted.
egze
Another common pattern is to write like this:
tfwright
I like this suggestion as well. It matches expectations created around function arguments by providing an intermediary step between writing a named function that takes arguments in the correct order and using an anymous function. Personally as I have learned elixir I have become accustomed to just writing a lot more function definitions because I am a pattern matching superfan so I’m not sure I would use it over a contextual convenience like noreply(), but I’d definitely try it out.
baldwindavid
That is typically how I write liveview replies too. I guess I see this as a lot more than just that use case though.
I am thinking it would be potentially even more useful for the cases where you want to pipe to the next function, but that function does not expect your piped value to be the first argument. @Qqwy Am I correct in assuming it would provide for this sort of thing?..
Qqwy
Thank you everyone for your responses!
Personally I find that kind of code (putting pipelines inside tuple- list- map- or struct-constructors) even harder to read than the ‘breaking the pipeline’-example of the original post. That’s definitely subjective, though.
Yes, you are correct. It was not my primary goal when building this, but indeed you can also use the ‘bare’
&1-syntax to pass the pipe result to any argument of a function as well.Thank you!
This is what I am hoping for.
And that might also address @ityonemo’s question: The reason I did not want to override another operator is because operators are not self-documenting. You do not know without context what
~>does in an Elixir-application, because it depends on what libraries are in scope. You do know what|>is (supposed to) do however, since it is built-in. I am trying to extend its semantics slightly while not breaking the ‘principle of least surprise’.I’d love to hear from more people if they agree with @baldwindavid or if they do find it too ‘magical’.
!
And feel free to shoot any holes in this implementation, of course
baldwindavid
One thing to look at are ways that it might feel awkward. I tried to put it through some paces via a nonsensical example and it looks nice to my eyes in various new and existing scenarios. (* denotes new function usage)
I also wonder if it causes any confusion regarding usage of captures outside the pipe operator. I can’t think of any good examples off-hand, but perhaps a developer might think the
&is somehow unnecessary for, say,Enumfunctions. I don’t know. It is still looking really good to me.ityonemo
Yeah, but that’s the point. In elixir it’s not hard to find out, since imports are lexically scoped.
For example, if you’re looking at code that uses my
net_addresslibrary, you might not know what~i"1.1.1.1"does, but you can make a pretty good guess, and then you can confirm by verifying that there’s animport IPstatement and looking up the code for that.It’s important to use something that isn’t the normal usage of the pipeline because you need to signal to the user that “something is different” here, otherwise people will scratch their heads even if it should be obvious what’s going on.
I was thinking about what you might want to replace
&1with, and I would pick__PIPE__. I don’t like &1 because I think it’s bad to confuse readers that it might be a capture, and 1 doesn’t make sense since there’s no way for it to be 2, 3, etc.__PIPE__will ast to an alias (atom), so it should be trappable, and there is precedence for such forms to not necessarily be elixir module names (__ENV__,__CALLER__,__STACKTRACE__)baldwindavid
I see your point there, but working in a pipe we know that we’re only working with the single first argument, so
&1makes sense to me. This might be a terrible idea, but it would also be understandable to me if I saw just&rather than&1. I still think&1is explicit and obvious though.Are you thinking about this from the standpoint of a language enhancement or as a library? If it is only ever to be a library then I can see introducing a new keyword. A developer would then immediately know that this is provided via a library.
I’m thinking about it with the mindset of it eventually being a language enhancement and, in that case, it seems like less mental overhead to use a subset of the already existing syntax. Getting this added to the language would obviously be a much heavier lift. My assumption is that @Qqwy at least has some visions of this being added to the language, but I might be wrong there.