Conditional piping part of the language?

Hello,
I keep using a paradigme using a homemade function to make my life easier when piping.
I wonder if there is a better way to achieve the same result, maybe something already part of Elixir language.

Instead of writing a tailored maybe_dothat() function, I prefer a generic maybe_do() wrap function around a do_that() function.

Here is the may_be/1 wrapper.

  def maybe_do(pipe_value, condition, function)
      when is_boolean(condition) and is_function(function) do
    if condition do
      function.(pipe_value)
    else
      pipe_value
    end
  end

Let’s say I want to always call do_this/1 on a socket and do_that/1 but only if my_bool is true, else socket rest unchanged.
I write it this way:

  socket
    |> maybe_do(my_bool, &do_this/1)
    |> do_that()

Your insights are welcome.
Jean-yves :slight_smile:

3 Likes

There is not such construct in Elixir that I know of, there is tap/2 to perform a side effect but the original value is the one being pipped, there is also then/2 but here is the function return in the second argument that is being pipped instead.

So I guess your solution is fine if you find yourself repeating that pattern a lot.

2 Likes

As the other poster said, tap and then cover a lot of needs, and also yeah if it’s more convenient for you to use your own function then please don’t feel bad about it! It’s very normal.

People use their own functions a lot when you need to go through a series of validations, for example. It’s expected to do it like that in many teams even.

6 Likes

You can do the same with case:

value
|> case do
  x when my_condition -> f.(x)
  x -> x

Alternatively you can write a macro that supports holes, lets say you call it p:

value
|> (p if my_condition, do: f.(_) else: _)

Doesn’t have to be exactly like this ofc, you can define it to be whatever you like the most. Just giving an example here. I actually have a bunch of these in my helpers libs. Edit: or, even better, you can also extend the pipe itself with this macro.

3 Likes

If you’re really talking about a socket and only talking about a couple of functions, I would use a live hook like on_mount or attach_hook along with an if.

1 Like

It’s about coding GUI behavior (Phoenix/Surface) based on user actions.

Yes, it does the same, but I like the compactness of maybe_do(…). It reads nicely.

Yes agree, case is a bit lengthy.

Here is a related discussion https://groups.google.com/g/elixir-lang-core/c/OGRW7R74ArY/

An alternative implementation:

def maybe_do(value, true, func) when is_function(func, 1), do: func.(value)
def maybe_do(value, _, _), do: value

6 Likes

That’s way better, thank you!
I still fear that my approach maybe a code smell.
I mean, I must not be the first one to need to chain (pipe) functions based on some conditions that are not directly part of the piped data.

This is typically true with GUI events. You want to reflect the actions in the socket (the piped data), but the event itself is not part of that socket.
An option would be to add the events to the piped data, piping {event,socket} rather than the plain socket.
Anyway, thank you again.

In my opinion, over use of then is a code smell; your maybe_do function is not.

3 Likes

It’s very hard to tell without seeing your exact code, but if you’re feeling it’s a code smell it’s possible that you are trying to force a pipeline when it is not needed. Once you start threading tuples like {event, socket} through a pipeline where only one function cares about event, you’re hiding details. It makes it harder in the future to read the pipeline at a glance without looking at the implementation of each function. Of course if it’s a pattern that appears everywhere it’s not such a big deal, but generally pipelines are best when they operate on a single immutable data structure. YMMV, of course.

5 Likes

Very informative discussion :grinning:

For this I have a little macro in my toolbox:

defmacro then?(value, pred, fun) do
  quote do
    if unquote(pred) do
      unquote(fun).(unquote(value))
    else
      unquote(value)
    end
  end
end

To me it’s a good complement to the existing then/2 macro. I tried to get it added 2 years ago, but no luck. :man_shrugging: :slight_smile:

2 Likes

I usually do this:

def if(value, conditional, _) when conditional in [nil, false], do: value
def if(value, _, fun), do: fun.(value)

Fair, but I don’t believe in “truthy” and “falsy” values. If it’s not a boolean then it’s an invalid input.

2 Likes

You are free to take that attitude, but you are leaving powerful functions for communicating intent like List.wrap/1 off the table.

I have something even eviler:

  defmacro value ~> name do
    quote do
      value = unquote(value)
      unquote(name) = if value, do: value, else: unquote(name)
    end
  end

  defmacro name <~ value do
    quote do
      value = unquote(value)
      unquote(name) = if value, do: value, else: unquote(name)
    end
  end

So I can do:

socket
|> step1()
|> step2()
~> socket

socket <~ if(condition?, do: step3(socket))

socket
|> step4()
|> step5()
1 Like

How so? I am curious. Could you give an example?

I cringe when I see stuff like if list ... when list should never be nil anyway. If a list might be nil (f.ex. if you can’t control it when a framework is calling your callback or such) then it’s best if you do call_function(List.wrap(list_or_nil), ...).

And I believe this is not just some random personal preference. I prefer code to communicate its intent clearly. Just doing if this_could_be_anything ... with no regard of its type is to me a code smell because it’s not clear what the intent is. Might as well write PHP or JS at that point. We should use all of Elixir’s strengths.

Furthermore, I’d prefer to strictly assert when is_list(parameter) in my function signatures as well (or use the [first | rest] pattern-matching idiom, optionally together with [] if an empty list is an acceptable parameter value).