Ankhers

Ankhers

Just a little information upfront. Generally speaking, if I feel like I need to either break a pipe chain or use an anonymous function in order to pipe into an arbitrary argument, I will create a named function instead. My code becomes a bit more verbose, but I think it often comes with additional clarity. With that said, Lets get on with the discussion.


There was recently another proposal in order to pipe into an arbitrary argument of a function. I’m not really here to discuss the validity of that proposal. That proposal did get me to think about a potential different approach (that may or may not have come up in the past, please point me to it if it was brought up) that may get us (hopefully) 80% of what people are looking for.

When I see these discussions crop up, I realize that people are not showing “real” code most of the time and are just coming up with toy examples to show off the proposed syntax. With that said, it appears that people want to pipe into the second parameter more frequently (again, it may just be toy examples though).

So I am mostly here to ask, if you had to take a guess (and if you have some time, maybe scour a codebase or two), what percentage of time are you trying to pipe to the 2nd argument vs 3rd+. I’m not really interested in piping to the first, because we already have a working solution for that.

If it turns out that most of the time people are looking to pipe to the 2nd argument, Maybe we could implement flip/2. The first argument is the value you want to become argument 2 of the second argument (that is comfusing…). Lets take a look at a couple examples

"foo"
|> String.upcase()
|> flip(Regex.scan(~r/foo/))

map
|> Map.get(:foo)
|> process()
|> flip(GenServer.call(pid, 10_000))

I got the idea from Haskell, though I’m sure something similar is in many languages. We are unable to do exactly the same thing because currying is not really a thing by default in Elixir. Haskell’s version is actually (a -> b -> c) -> b -> a -> c, which roughly transaltes to the first argument being a function that takes 2 arguments, the secound argument is some value b and the third argument being some value a. Roughly translated to Elixir could look something like fn (fun, b, a) -> fun.(a, b) end

flip/2 would really only be intended to be used inside a pipe chain, because writing flip("foo", Regex.scan(~r/foo/)) is not as clear as Regex.scan(~r/foo/, "foo")

So if anyone has any thoughts on flip/2, I would love to hear them. And if anyone can either go through some code, or take a guess at percentages for 2nd vs above 2nd argument for pipes, it would be appreciated.

Also, if you would like to test it out in some code, I threw the flip package up onto hex and you can find the code on github.

Showing Posts 1 to 10

OvermindDL1

OvermindDL1

Flip is nice when the language has auto partial application (no curry’ing needed), but Elixir does not have that, mostly owing to lacking a typing system, thus flip becomes too difficult to read.

I’m still personally a fan of just placing a _ as a “hole” (both visually and logically) for the pipe to fill, which would allow you to do things like:

something
|> blah(42, _)
|> blorp(_) # I like being explicit, no magical 'place in front' magicalness
|> vwoop.("test", _, "thing", _) # Can even be used multiple times!

But it is a nice obvious hole, both visually (“Oh, this is a piped into!”) and logically (scan the AST for _ and just replace it with a binding to the prior expression, single line of work).

egze

egze

I wonder how hard would it be to build it. Looks great!

taiansu

taiansu

Actually I wrote a plugin named pipe_to which makes it possible to specified the position of argument while pipe, but only one argument for now.

OvermindDL1

OvermindDL1

Oh there is already a library out somewhere that adds the _ ability to pipes, forgot it’s name, but it’s really really easy to code, let me whip up an untested and probably somehow wrong example in the console:

╰─➤  iex
Erlang/OTP 21 [erts-10.2.1] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [hipe]

Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> defmodule BetterPipe do  
...(1)>   def better_piping(value, call, i) do
...(1)>     # Obviously come up with a better reusable binding name, purely temporary and probably still not scope clean in some cases 
...(1)>     binding = Macro.var(:"$bpb$#{i}", __MODULE__)
...(1)>     {call, n} = Macro.prewalk(call, 0, fn
...(1)>       {:_, _meta, ctx}, n when is_atom(ctx) -> {binding, n + 1}
...(1)>       {:|>, _meta, [v, c]}, n -> {better_piping(v, c, i + 1), n}
...(1)>       ast, n -> {ast, n}
...(1)>     end)
...(1)>     cond do
...(1)>       n == 0 -> Macro.pipe(value, call, 0)
...(1)>       n == 1 -> Macro.prewalk(call, fn ^binding -> value; ast -> ast end)
...(1)>       n -> quote do unquote(binding) = unquote(value); unquote(call) end
...(1)>     end
...(1)>   end
...(1)> 
...(1)>   defmacro value |> call do
...(1)>     BetterPipe.better_piping(value, call, 0)
...(1)>   end
...(1)> 
...(1)>   defmacro __using__(_) do
...(1)>     quote do
...(1)>       import Kernel, except: [|>: 2]
...(1)>       import BetterPipe, only: [|>: 2]
...(1)>     end
...(1)>   end
...(1)> end
{:module, BetterPipe,
 <<70, 79, 82, 49, 0, 0, 11, 108, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 96,
   0, 0, 0, 36, 17, 69, 108, 105, 120, 105, 114, 46, 66, 101, 116, 116, 101,
   114, 80, 105, 112, 101, 8, 95, 95, 105, 110, ...>>, {:__using__, 1}}

And it is thus used as:

iex(2)> use BetterPipe
BetterPipe
iex(3)> "test" |> String.upcase() # Normal style works
"TEST"
iex(4)> "test" |> String.upcase(_) # Or use a hole
"TEST"
iex(5)> "test" |> IO.inspect(_, label: _) # Or use many holes, even inside other expressions
test: "test"
"test"
iex(6)> "test" |> IO.inspect(_, label: _) |> String.upcase(_) |> IO.inspect() |> String.downcase() |> IO.inspect(_, label: _)
test: "test"
"TEST"
test: "test"
"test"
iex(13)> ["1", "22", "333"] |> Enum.find(_, fn v -> v |> Regex.match?(~R/^[\d][\d]$/, _) end) # Recursive is no problem either
"22"

Supports full multi-positioning, etc… They are simple to write (although elixir’s language has fun with corner cases, needs tests!). And it is so much more clear to me, no magical values being stuffed into positions (which is weird in a non-curried language, does it go to the start, end, middle, what?!)!

Ah right, it was your library!

Why only one? And it doesn’t seem to support piping into a structure in an argument either, or just a structure outright?

With mine above for example:

iex(14)> 1 |> %{a: _}
%{a: 1}
iex(15)> [a: 42] |> %{a: _[:a]}
%{a: 42}

It is generic piping into something, not just function calls! :slight_smile:

I really should publish this sometime, with tests and all, I keep remaking it almost verbatim every few months… >.>

10
Post #4
taiansu

taiansu

Thanks for the great tips! And I suddenly realize what does it mean by use two target locations. It doesn’t mean return a curried function, but just replace both of target locations with the same, previous result, right? I’ll find some time to implement these.

hauleth

hauleth

Yeah, I have written something similar. One place where I can see it fails is when you pipe to something with a body, for example:

foo
|> case do
  {:ok, a} -> a
  _ -> throw "Invalid call"
end
OvermindDL1

OvermindDL1

Yeah, mine is definitely not complete, probably should expand macros and not go into blocks or such, or maybe it should just should use another ‘hole’ like __ or something.

aenglisc

aenglisc

In my experience a fair amount of piping inconveniences occurred when I needed to use some Erlang function (say, crypto:hmac/3) where the data that needs to be piped tends to be at the end of the argument list and thus can end up virtually anywhere. Although there generally aren’t that many functions with 3 or more arguments of course.

OvermindDL1

OvermindDL1

Yeah, Erlang is much more usual with the subject being at the ‘end’, Elixir is a weird one with it being at the front, if it matched erlang then ‘most’ of these issues (though still not all) wouldn’t be an issue. ^.^

hauleth

hauleth

In Erlang isn’t that consistent in that matter either, ex:

  • lists:key* functions use 3rd argument as a “source”, which is normally last, except lists:keyreplace/4
  • binary:match/{2,3} uses 1st argument as as “source” and 2nd as a pattern, just like Elixir
  • ets:match/{2,3} uses 1st argument as a “source” tab and 2nd as a pattern
  • erlang:append/2 uses 1st argument as a “source” and second as a value, just like Elixir’s Tuple.append/2
  • maps:merge/2 works like Elixir, but to be consistent with “last argument is source” it should work other way around
  • maps:update_with/{2,3} has optional Init argument in the middle, but maps:get/{2,3} has optional argument as a last one

Not to brag about Erlang, but Elixir seems to be more consistent there about position of “source”.

Where Next? Top

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91898 914
New
AstonJ
The obligatory hello world thread! Who are you and where are you from? :stuck_out_tongue:
4616 55835 594
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New
Herve37
We’re evaluating API mocking tools for OpenAPI-based projects and would love to hear what other teams are using. We’re particularly inte...
New
matt-savvy
Is there a word for the ~> symbol used in Version strings? Do you also just call it a Squiggle Arrow™ ?!
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews