Nicer way to pipe?

This is basically the same question I’ve asked before, I’m just wondering if anything has changed since I last asked, or if there’s any other ways to do this thing that I am doing.

I know there’s been proposals for alternate ways to pipe some input into an argument slot other than the first position, and I understand why those got shot down, so I’m not really asking about that.

Here’s an example of a function I wrote recently, and shows a style I’ve been doing kind of frequently.

  def get_meter(num, total) do
    Decimal.div(num, total)
    |> Decimal.mult(10)
    |> Decimal.round(0)
    |> Decimal.to_integer()
    |> (fn level ->
      Enum.map(0..10, fn ^level -> "|"; _ -> "-" end)
      |> Enum.join()
    end).()
  end

And this is another way to do the same thing, which I kind of like because we don’t have to add the extra parentheses, but we do have to add an extra level of indentation, which I find annoying.

  def get_meter(num, total) do
    Decimal.div(num, total)
    |> Decimal.mult(10)
    |> Decimal.round(0)
    |> Decimal.to_integer()
    |> case do
        level ->
          Enum.map(0..10, fn ^level -> "|"; _ -> "-" end)
          |> Enum.join()
    end
  end

Using case is really nice when we truly have different conditions to handle in different ways, and I love that we can then pipe the result from the case statement/operator/function/macro/whatever-the-correct-term-is into whatever else comes after, but in the example shown, I don’t like it much.

The best alternative I can find to make this a little cleaner would be this:

  def do_func(arg, func), do: func.(arg)

  def get_meter(num, total) do
    Decimal.div(num, total)
    |> Decimal.mult(10)
    |> Decimal.round(0)
    |> Decimal.to_integer()
    |> do_func(fn level ->
      Enum.map(0..10, fn ^level -> "|"; _ -> "-" end)
      |> Enum.join()
    end)
  end

Would be nice if Elixir had a built-in Kernel.do_func/2.

1 Like

Oh hey! This is not bad! Just realized I could make this small change.

  def do_func(arg, func), do: func.(arg)

  def get_meter(num, total) do
    Decimal.div(num, total)
    |> Decimal.mult(10)
    |> Decimal.round(0)
    |> Decimal.to_integer()
    |> do_func(&Enum.map(0..10, fn ^&1 -> "|"; _ -> "-" end))
    |> Enum.join()
  end
1 Like

Another approach could be to use capture operator & to create anonymous function. Plus after it we could take Enum.join/1 out to the “main pipeline”

  def get_meter(num, total) do
    Decimal.div(num, total)
    |> Decimal.mult(10)
    |> Decimal.round(0)
    |> Decimal.to_integer()
    |> (&Enum.map(0..10, fn ^&1 -> "|"; _ -> "-" end)).()
    |> Enum.join()
  end

However, I think overall this pattern of wrapping code into anonymous function just to fit into pipeline is considered as bad a bad practice and it’s more preferred to break it into separate functions or intermediate values
(see https://github.com/lexmag/elixir-style-guide#anonymous-pipeline

3 Likes

There ya go! =)

Sorry to have cut in front of you @RudManusachi.

I hear what you’re saying, but this seems it’s pretty small to break out into a separate function. Mind showing me what you think would be the best practice for this?

Exactly. It is not obvious what the intent of (&Enum.map(0..10, fn ^&1 -> "|"; _ -> "-" end)).() is at all. However if you name it, then it’s a lot clearer.

5 Likes

'Aight, good feedback. You all were right, I moved some things around and the code got better. Thanks everyone.

3 Likes