How to check for a condition inside a pipe chain?

Inside a pipe chain, how to execute one pipe line / exclude it based on say is_list(variable)? i.e. execute pipe line 5 only if variable is a list. Is that possible?

3 Likes

Do you have an example of the code? I guess that with the code is a little bit better to understand what you mean. :slight_smile:

1 Like

I have discovered a possible solution… Here is the working pipeline with conditionals:

  page =
  Policy
    |> join(:left, [p], c in Client, p.client_id == c.id)
    |> where([p, c], c.user_id == ^conn.assigns.current_user.id)

  # filter by expires
  if is_nil(params["policy"]["expires"]) do
    expires_params = "01-01-1970" |> Timex.parse!("%d-%m-%Y", :strftime) |> Ecto.DateTime.cast!
    page = page |> where([p, c], p.expires >= ^expires_params)
  else
    page = page |> where([p, c], p.expires >= ^params["policy"]["expires"])
  end

  # filter by client_id
  if is_list(params["policy"]["client_id"]) do
    page = page |> where([p, c], p.client_id in ^params["policy"]["client_id"])
  end

  # final processing
  page = page |> preload([p, c], [client: c]) |> Repo.paginate(params)

Works, but maybe there is a better / nicer way to code it :slight_smile:

Actually, you should never assign variables inside conditionals like if, case and so on.

Instead of writing:

if is_list(params["policy"]["client_id"]) do
  page = page |> where([p, c], p.client_id in ^params["policy"]["client_id"])
end

You should write:

page = 
  if is_list(params["policy"]["client_id"]) do
    page |> where([p, c], p.client_id in ^params["policy"]["client_id"])
  else
    page
  end

Other than that I think it’s a good solution, but you could also take a look at Ecto’s 2.1 new dynamic expressions: https://github.com/elixir-ecto/ecto/blob/v2.1/CHANGELOG.md

3 Likes

It might be possible to extract a function where you can pattern match and compose the query. Here’s what it could look like for the expires policy (actual code may depend on where your functions and usage are located):

# Policy module
def with_expiry(page, nil) do
  expires_params = "01-01-1970" |> Timex.parse!("%d-%m-%Y", :strftime) |> Ecto.DateTime.cast!
  page |> where([p, c], p.expires >= ^expires_params)
end
def with_expiry(page, expires_params) do
  page |> where([p, c], p.expires >= ^expires_params)
end

#...

page =
  Policy
    |> join(:left, [p], c in Client, p.client_id == c.id)
    |> where([p, c], c.user_id == ^conn.assigns.current_user.id)
    |> Policy.with_expiry(params["policy"]["expires"])
    # ...
8 Likes

@rossta, @vidalraphael, @kelvinst

Thank you very much… I have learned new things here today… I am happy that I have found my way back to this forum after some bad bi yearly banning experiences at holy stackoverflow (latest only because I asked a duplicate question)…

4 Likes

Yep! @rossta thank you! That’s the way to go!

2 Likes

A post was split to a new topic: StackOverflow culture

Hi there!

If I have to pass in a limit clause based on a function argument. Is function pattern matching the best way to do this? In the solution above, there is 2 functions that returns where clause. Is it possible to do

def with_limit(page, nil) do
page
end

def with_limit(page, limit_size) do
page |> limit: size
end

query |> with_limit limit_size

i was going to do a
if param do
page = query |> limit: size
end

but someone mentioned earlier that i shouldnt be assigning in if/else clause.

I’m often lazy and just put a case between things… >.>

value
|> something(42)
|> something_else()
|> case do
     page where limit_size == nil -> page
     page -> page |> limit(size)
   end
|> some_more_things()
...

Also you really need to use code tags instead of quotes for bodies of code, you do it like:
```elixir
Code goes here like 2+2
```
Makes:

Code goes here like 2+2
4 Likes

heheh thanks! :stuck_out_tongue: you saved me again man! :stuck_out_tongue: will try it out!

IO.puts "Thanks!"
2 Likes

Hehe, do note the ‘proper’ way to do it is different function heads and pipe to the function, but when lazy… ^.^;

Woah. Did not realize you can do this. So much more readable than anon functions. Time to go clean up some code :smiley:

1 Like

Bringing the discussion up, as it’s been a while I felt like I was missing something, and I may finally have nailed it.

Context

As far as I understand, the philosophy of a functional programming language is about data transformation.

Hence the pipe operator is amazing in Elixir as it is exactly what it does:

initial_data
|> first_transformation()
|> second_transformation()
|> third_transformation_with_params(my_param)
|> another_one()
# final_data

But there are cases where the transformation should be applied conditionally, which is what is discussed here.

Of course, one can write another sub-method like:

def maybe_apply_second_transformation(data, true), do: second_transformation(data)
def maybe_apply_second_transformation(data, false), do: data

initial_data
|> first_transformation()
|> maybe_apply_second_transformation(should_apply)
|> third_transformation_with_params(my_param)
|> another_one()

But that’s way too much extra code for a simple condition.

So I like @OvermindDL1 quite a lot:

initial_data
|> first_transformation()
|> case do
  data when should_apply -> second_transformation(data)
  data -> data
end

But this syntax is quite heavy for a simple if, the data -> data line doesn’t bring any valuable information, not perfect IMHO.

Question

Shouldn’t this be valid:

initial_data
|> first_transformation()
|> (if should_apply, do: second_transformation())
|> third_transformation_with_params(my_param)
|> another_one()

I know it doesn’t now, I guess, the data is given as the first parameter of if, instead of should_apply.

But when writing a if in a pipeline, it is more useful to get the data as the first parameter of the do block than of the if condition.

# Not useful
boolean
|> if do: data

# More useful Useful
data
|> if boolean, do: fn d -> d end
# data

If the if operator shouldn’t be used for backward compatibility reasons, shouldn’t we imagine another one, like if_map? Or even a syntax like:

initial_data
|> first_transformation()
|>? should_apply, do: second_transformation()
|> third_transformation_with_params(my_param)
|> another_one()

Thanks for reading and feedback

I feel that statement is the trap here. Instead of naming it maybe_apply_second_transformation I’d suggest something like prepare_for_third_transformation (maybe inline second_transformation(data)) and then the code is totally fine. Most importantly that code will age the best. Say at some later point you also need to do some different transformation for the false case as well. Just replace data with whatever that other case needs to be applied to.

All your proposed short inline forms will likely no longer be short and sweet once you need to deal with 2 cases and once you’re no longer at 1 or 2 cases you’ll need to switch to the function (or case) version anyways.

As a possible alternative to writing a full fledged maybe_* function, you can also consider to use a general purpose maybe_if/3 function. I’ve done that in the past when I needed to chain multiple optional steps.

initial_data
|> first_transformation()
|> maybe_if(should_apply_step2, &second_transformation/1)
|> maybe_if(should_apply_step3, &third_transformation_with_params(&1, my_param))
|> another_one()

where maybe_if is defined like this:

def maybe_if(data, true, action)
  when is_function(action, 1),
  do: apply(action, data)

def maybe_if(data, false, _action), do: data
4 Likes

Thanks for feedback that’s very interesting.

@wolf4earth I like your solution very much, I shall go for it. :smile:

All your proposed short inline forms will likely no longer be short and sweet once you need to deal with 2 cases and once you’re no longer at 1 or 2 cases you’ll need to switch to the function (or case) version anyways.

@LostKobrakai I don’t really agree with your point.

data
|> first_transformation()
|> if should_apply, 
      do: second_transformation(), 
      else: alternate_transformation()
|> third_transformation()

reads much clearer IMHO than

data
|> first_transformation()
|> case do
  data when should_apply -> second_transformation(data)
  data -> alternate_transformation(data)
end
|> third_transformation()

My thinking made me understand that in order to get my ideal version to work, one should write the if/4 macro (from if source code). Apologies I suck at macros… :disappointed:

  defp build_if(nil, condition, do: do_clauses, else: else_clauses) do
    build_if(condition, do: do_clauses, else: else_clauses)
  end

  defp build_if(entry, condition, do: do_clauses, else: else_clauses) do
    quote do
      if(condition, entry |> unquote(do_clauses), entry |> unquote(else_clauses))
    end
  end

Though my code may not be correct, couldn’t it work?

The cool thing is that from my understanding, it could be backward compatible, as it is easy to detect that entry is nil, so the very same operator could be used (though it may be confusing at first).

I’d say so as well, but imo non of that needs to be handled as part of the pipeline. The pipeline should only be concerned with the steps needing to happen, but not with what happens as part of those steps – and doing nothing is a valid option for the latter.

For your examples second_transformation and alternate_transformation are both versions of some_transformation_needed_before_the_third_transformation, which should be what’s piped into.

I guess I’m not yet comfortable with the expressiveness of idiomatic Elixir code.

Take this SO question.

It seems like the solution in Elixir is to always create more functions, whereas I’m a one-liner fan, as context always varies, and on-the-fly definitions seem more appropriate to me than more code lines.

But this would be a long topic to discuss!