A hybrid between a cond statement and a multi, does it exist?

So when writing logic that is too complex for a case, (usually with C it will look like a function with return statements scattered throughout) a cond is used.

Now one thing that irks me about cond is, you cannot assign variables in the branches.

Multis for example will fit the bill, but they are a bit heavyweight for the simple usecase of just wanting to assign a variable in a cond.

Here is an example:

inventory = state.inventory
cond do
    Enum.find(inventory, & &1.name == "Moon Cake") ->
      moon_cake = Enum.find(inventory, & &1.name == "Moon Cake")
      {:eat, moon_cake.id}
    (Enum.find(inventory, & &1.name == "Space Cake")[:count]||0) > 10 ->
      space_cake = Enum.find(inventory, & &1.name == "Space Cake")
      {:eat, space_cake.id}
   true -> 
     nil
end

But wish we can do

cond do
    moon_cake = Enum.find(inventory, & &1.name == "Moon Cake") ->
      {:eat, moon_cake.id}
    (space_cake = Enum.find(inventory, & &1.name == "Space Cake"))[:count]||0 > 10 ->
      {:eat, space_cake.id}
   true -> 
     nil
end

Is there any good solutions out there? The net win is that, the cond does not go further, so as to prevent multiple iterations of the inventory if an earlier clause succeeded.

Wouldn’t Enum.find_value/3 be more appropriate here? Something along the lines:

Enum.find_value(inventory, fn
  %{id: id, name: "Moon Cake"} -> {:eat, id}
  %{id: id, name: "Space Cake", count: count} when count > 10 -> {:eat, id}
  _ -> nil
end)
2 Likes

Enum.find_value is an interesting one, I will start using this thanks! But not fitting to this usecase.

EDIT: Oh a problem with find_value is priority, say we wanna ALWAYS eat the Moon Cake (if we have it) before the Space Cake. Depending on the order of the elements in the inventory that is not guaranteed.

In this particular case imagine the conditional is more complex with many more checks, for example:

cond do
    is_dead(state) -> nil
    is_full(state) -> nil
   ..
end

Using Enum.find_value we will be checking is_dead and is_full on every iteration.

You can do something along the lines of

Enum.reduce_while(inventory, nil, fn                                                                                                                       
  %{id: id, name: "Moon Cake"}, _ -> {:halt, {:eat, id}}                                                                                                     
  %{id: id, name: "Space Cake", count: count}, _ when count > 10 -> {:cont, {:eat, id}}                                                                      
  _, acc -> {:cont, acc}                                                                                                                                     
end)

For anyone else like myself who somehow missed this syntax when they were in elixir school: pattern matching on anonymous functions

I am now cringing thinking of all the fn arg -> case arg do end end in my code :man_facepalming:

1 Like

This is getting there but imagine you have an inventory and warehouse now. You want to check if the Moon Cake is in the inventory, then also in the warehouse, in the same cond. So reducing just the inventory is not enough.

@tfwright, Haha its nice.

Sounds to me like you can just reduce both in one go.

I am not seeing it, at least not cleanly. And again the reduce_while loses the order, so if we want to ALWAYS eat Moon Cake BEFORE Space Cake if we have both, the reduce_while does not fit the bill.

It doesn’t, it terminates as soon as you find a Moon Cake.

[
  %{count: 12, id: 1, name: "Space Cake"},
  %{id: 2, name: "Moon Cake"},
  %{count: 8, id: 3, name: "Space Cake"},
  %{count: 16, id: 4, name: "Space Cake"},
  %{count: 3, id: 5, name: "Space Cake"},
  %{count: 11, id: 6, name: "Space Cake"},
  %{count: 1, id: 7, name: "Space Cake"},
  %{count: 15, id: 8, name: "Space Cake"},
  %{count: 7, id: 9, name: "Space Cake"},
  %{count: 18, id: 10, name: "Space Cake"}
]
iex(19)> Enum.reduce_while(inventory, nil, fn                                                                                                                       
...(19)>   %{id: id, name: "Moon Cake"}, _ -> {:halt, {:eat, id}}                                                                                                     
...(19)>   %{id: id, name: "Space Cake", count: count}, _ when count > 10 -> {:cont, {:eat, id}}                                                                      
...(19)>   _, acc -> {:cont, acc}                                                                                                                                     
...(19)> end)
{:eat, 2}

Sigh, but this is just fitting a ironcast mold to the exact problem, with the cont and halt. Okay new requirement there is a Blackhole Cake now. And it should be prioritized last (so if have Moon Cake, Space Cake and Blackhole Cake) eat only 1 in that priority.

All of a sudden this new requirement which is very practical as the system grows is going to lead to a nasty refactor.

Does not sound like a big deal honestly.
Store the whole map in the acc and build your logic around that.

You’re second example pretty much works as is, I occasionally use a similar pattern.

I just added an extra paren for the “Space Cake” clause:

    inventory = [%{id: 1, name: "Space Cake", count: 11}]

    cond do
      moon_cake = Enum.find(inventory, &(&1.name == "Moon Cake")) ->
        {:eat, moon_cake.id}

      ((space_cake = Enum.find(inventory, &(&1.name == "Space Cake")))[:count] || 0) > 10 ->
        {:eat, space_cake.id}

      true ->
        nil
    end

Although if you can structure it as a single reduce it will be more performant (although you’ll need quite a long list before that will begin to matter much), but turning this into a reduce might make it more difficult to follow since it doesn’t seem like the operation you are doing is easily thought of as a transformation. Also if the clause is as complex as the “Space Cakes” clause I would probably think about restructuring the logic away from a cond, or maybe extracting a helper function.

1 Like

Wow cool. How did I miss that.

Another thing missing thought is being able to treat it kinda like a fusion with a with statement, in that earlier assignments can be accessed later.

    cond do
      moon_cake = Enum.find(inventory, &(&1.name == "Moon Cake")) ->
        {:eat, moon_cake.id}

      ((space_cake = Enum.find(inventory, &(&1.name == "Space Cake")))[:count] || 0) > 10 ->
        {:eat, space_cake.id}

      !!space_cake and space_cake.count > 5 ->
        {:cook, space_cake.id}

      true ->
        nil
    end

Probably not so clean but just using for example purposes.

Often I use what I call the “reverse with” form:

with :error <- try_this(),
     :error <- try_that(),
     :error <- try_other_stuff() do
  :error
else
  {:ok, _} = ok -> ok
end

In your case, that could be:

with nil <- Enum.find(inventory, &(&1.name == "Moon Cake")),
     nil <- Enum.find(inventory, &(&1.name == "Space Cake" && (&1[:count] || 0) > 10)) do
  nil
else
  item -> {:eat, item.id}
end

Hi, I believe you’re trying to find a language feature that will exactly solve your particular business problem. I think that there’s an inherent mismatch with this approach as languages provide low-level tools that can be composed together to solve bigger business problems, they do not provide off-the-shelf solutions for every different business problem imaginable.

I believe that you could use most of the solutions already mentioned as most of them are quite extendable and can be made to fit the current problem as well as future changing requirements (like the Blackhole Cake curve balls that business people are so adept at throwing when you least expect them).

For what it’s worth, I believe you could break your problem down into 2 separate phases, the discovery and the action phases. You can Enum.reduce/3 over you inventory taking notes of which items you discovered (so all items, not a quick exit) and then decide on the importance/precedence of those findings with a simple if/else (or similar) decision making process

Yea this can work to an extent but its hard to read. I tried this pattern before but did not like that it reads very poorly and its hard to figure out what is going on since all the implications of the with (else clause and silently returning the match failure by default; I feel the last part causes chaos in a loosely typed language)

its also possible to assign then check in the with as well

inventory = [%{id: 1, name: "Space Cake", count: 11}]
with moon_cake <- Enum.find(inventory, & &1.name == "Moon Cake"),
     true <- is_nil(moon_cake) || {:eat, moon_cake.id},
     space_cake <- Enum.find(inventory, & &1.name == "Space Cake"),
     true <- is_nil(space_cake) || space_cake.count < 10 || {:eat, space_cake.id} do 
end

@bottlenecked

Yea definitely, but the with statement was created for exactly that. So if there is a nice way to solve a pattern it seems plausible to consider it at least as a feature. The reason why creating multiple guard-like functions does not work is that there are just so many variations and combinations that you end up with small functions that get passed 5+ arguments, then you start thinking maybe I should just pass a state object to each, which then leads to nightmare refactors if that state object structure changes. (Same nightmare refactor problem I see with ecto multis, tho the new 1.11 map key precompile checks might remedy this abit)

You can Enum.reduce/3… yes this is a fine pattern and occasionally we use it but its still not ideal from a readability perspective IMO. Readability and maintainability is always subject to debate tho. On this line maybe to take this a step further, we should redesign our thinking and perhaps always use this type of pattern with a weight assigned to each action result. Then sort by weight and take the first. Kind of a GOAP like pattern.