ParamPipe - parameterized pipe in Elixir

parameterized pipe in elixir: |n>

edit: negative index in |n> and mixed usage with |> are supported

example:

  use ParamPipe

  def foo(a, b, c) do
    a*2 + b*3 + c*4
  end

  def bar0() do
    100 |> div(5) |> div(2) # 10
  end

  # negative n in |n> is supported 

  def bar1() do
    1 |0> foo(0, 0) |1> foo(0, 0) |-1> foo(0, 0) # 24
  end

  # mixed usage with |> is supported

  def bar2() do
    1 
    |> foo(0, 0) # 2
    |1> foo(0, 0) # 6
    |> div(2) # 3
    |> div(3) # 1
    |2> foo(0, 0) # 4
    |> (fn x -> foo(0, 0, x) end).() # 16
    |-1> foo(0, 0) # 64
  end

code: http://github.com/cjen07/param_pipe

what do you think of this? :slight_smile:

previous tricks: 3 meta-programming demos: prefix, memorization, julia port

1 Like

It is difficult to exaggerate how much discussion has been had with respect to modifying the pipe operator. The consensus response is pretty negative to such modifications, and to be honest this is a great example why.

1
|0> foo(0, 0)
|1> foo(0, 0)
|2> foo(0, 0)

This is completely indecipherable. Obviously the foo function name doesnā€™t help, but the whole point of the pipe operator is that there is a consistent view on what constitutes the primary noun for each function.

6 Likes

why? |n> just passes the previous result as the nth parameter to the function it pipes to.

If you want to pass the content directly to File.write!, you can use content |1> File.write!("path").

Instead of content |1> File.write!(path) I do prefer content |> to_file(path).

When I first read about a pipe that allows to pipe into an arbitrary position, I really was keen of the idea, but after a couple of years dealing with the default pipe, I started to learn to write helper functions, which make my intend much more clear, than a number that no-one ever sees on a quick read inbetween | and >. Even worse, people are so used to |> nowadays, that they will read |x> as |>! At least if they arenā€™t using fonts and editors that aid to differ both.

4 Likes

I agree that |> is more popular and more straightforward, and works pretty cool with helper functions. I am too lazy to write helper functions which is used just once, for all you need to do is to add a number between | and >. I am working on its mixed usage with the original pipe operator.

I need to do even more! As ParamPipe currently works, I have to use it (ok, acceptable) and give up Kernel.|>/2 completely! The latter is a nogo to me, as the visual difference between |1> and |0> is not high enough for me.

And the point I wanted to make, is not the helper function perse, but in fact, that you can often come up with much better readability by using helper functions. My version makes totally clear what happens, while with your ParapmPiped version, I have to realise, that it is parampiped, and I have to remember what the argument with the number 1 does. I even have to remember that you are indexing by 0, when it is much more common to refer to positional arguments as the first, second, third, etc instead of their indexā€¦

1 Like

I like it: elegant and concise solution to a common problem. It can coexist with regular pipes and the eye immediately catches when a special syntax is used. I believe (some way of doing) this will become standard in the future languages.

While some of the cases when you want to pipe to a non-first argument would benefit from changing the argument order (the standard-ness of data as first argument eases the cognitive load), there are some examples where you want this. E.g. https://github.com/tpoulsen/focus (lenses) project recently introduced another set of function heads: https://git.io/vQxFu - to combat just this. Notice how the author expresses his doubts about ambiguity of such approach. Making it easy to pipe to any argument (without using an ugly inline function) would generalize a solution to this problem.

1 Like

I just upgraded the code, |n> mixed usage with |> is supported.

Every line has to be re-interpreted. You have to mentally move everything around to figure out whatā€™s going where. Reading this is a matter of doing:

1 # ok this will be the first argument
|0> foo(0, 0) # last line is first argument, yay nice left to right top to bottom reading
|1> foo(0, 0) # last line is now middle argument ok so first 0 is first argument then we have the last line, and then the last 0 is the last argument.
|2> foo(0, 0) # last line is 2nd argument so the two right 0s are actually the LEFT most argument even though they're all on the right hand side.

Notice that by the end, weā€™re reading from right to left, inside to out, which is precisely the whole problem that the pipe was designed to eliminate! At least with foo(0, 0, foo()) I can keep reading positions as normal.

This has been discussed more times and with more posts than I can count, and while that isnā€™t your fault, it does mean that I just donā€™t have the energy to have this conversation again. For anyone considering pursuing this further please go read the other posts on this on the forum and the elixir lang core mailing list.

3 Likes

At least when I read this line, I know foo has three arguments and I am piping in the first one. Perhaps, this foo function is a good example to illustrate the problem of readability.

I have not read any conversations on modifications of |>, what I know is,

1: It is impossible to define an arbitrary infix operator in elixir not by recompiling it,
2: I love the syntax of |>, so I want to keep it the most similar way as far as I can by redefining |.
3: The third argument of Macro.pipe is always 0, except in macro_test.exs file.

I aim not to argue on readability, just a nontrivial solution to a problem in elixir I think. :slight_smile:

If I were to do something like this Iā€™d have the syntax as something like:

1
|> foo(0, 0) # 2
|> foo(0, &_, 0) # 6
|> div(2) # 3
|> div(3) # 1
|> foo(0, 0, &_) # 4
|> (fn x -> foo(0, 0, x) end).() # 16
|> foo(&_, 0, 0) # 32

Which is valid syntax and is syntax Iā€™ve made but I donā€™t use since it is not ā€˜standardā€™ anyway, plus it is not that hard to just do 1 |> (&foo(0, &1, 0)).() or so, as ugly as it is. Maybe just a helper macro to re-shuffle args or so would be cleaner or something.

Honestly, if I were to buff pipe, adding some ā€˜put in this place hereā€™ would be nice, but Iā€™d prefer something of a more monadā€™y composition as error handling then would become so much easier.

7 Likes

cool! could you show me how you defined &_?

what do you mean by monadā€™y composition?

1 Like

@OvermindDL1: &_ is awesome idea! I also think about ā€œupdateā€ pipe operator, but I have similar feeling like others about integer between | and >.

1 Like

Just an update: negative index in |n> and mixed usage with |> are supported!

I donā€™t, rather you overload the |> pipe operator and just override calls inside it that contain that. It is actually a pretty trivial to do transformation.

Like you know how the primary error handling in Erlang/Elixir are tagged tuples? Like you get :ok or {:ok, value} for a success result and a :error or {:error, reason} for a failure result and you have to pattern match on those with, say, case or so? Well that is a great pattern, and the BEAM as of OTP20 is even more optimized for these conditions, however it sucks if you want to, say, pipe the success value along but not the error conditions, like say this:

def safe_div(_n, 0), do: {:error, "Cannot divide by zero"}
def safe_div(n, d), do: {:ok, div(n, d)}

32
|> safe_div(4) # {:ok, 8}
|> safe_div(0) # {:error, "Cannot divide by zero"}
|> safe_div(2) # {:error, "Cannot divide by zero"}
|> map_error(16) # {:ok, 16}
|> return() # 16

And so forth or so. And it would still work with normal values, and would be overridable for all sorts of other things like success tuples, returning exceptions or so, etcā€¦

The exceptional library already adds this ability to the pipe (optionally overridden, otherwise it also defines ~> by default).

2 Likes

Not exactly, I just overload | operator.

exceptional library is cool!

Not if you want to specify an optional &_ inline. ^.^

an nontrivial update

assigning a variable within the pipe operator is supported

  def foo(a, b, c) do
    a*2 + b*3 + c*4
  end

  def bar3() do
    h =
      1
      |-2> foo(0, 0) = f # 3 = f
      |-1> foo(0, 0) # 12
      |> foo(0, 0) = g # 24 = g
      |-1> foo(0, 0) # 96
      |> foo(f, g) # 297
    IO.inspect(h+1) # 298
    :ok
  end
1 Like

I do admire that you wrote this library, but it looks certainly much harder to read that classic pipe operator :slight_smile:

  def bar3() do
    h =
      1
      |-2> foo(0, 0) = f # 3 = f
      |-1> foo(0, 0) # 12
      |> foo(0, 0) = g # 24 = g
      |-1> foo(0, 0) # 96
      |> foo(f, g) # 297
    IO.inspect(h+1) # 298
    :ok
  end

I apologize for being such a downer, but this is completely indecipherable. How is this supposed to be an improvement over over the unwinded form?

f = foo(0, 0, 1)
f = foo(0, f, 0)
g = foo(f, 0, 0)
h = foo(g, f, g)

This seems to just prove all the concerns folks had about expanding on |>.

1 Like