Binding vars inside `cond`?

I have something like (dummy code - real conditions vary):

cond do
	x !== a_non_expensive_function() -> cond0_error()
	a !== an_expensive_function_returning_b() -> cond1_error()
	c < an_expensive_function_returning_b() -> cond2_error()
	[…]
	true -> no_errors()
end

What’s wrong with this picture? Both cond1 and cond2 use the same expensive function, wasting cycles calling it twice. Sure I could call it only once, before the block, and bind the result to a variable used later inside the cond. Yet, since it is an expensive call I want to make it only when I am sure I have to. Meaning only if cond0 does not trigger an error before I get to checking cond1. This would not be a problem if I could bind the result of the first call (in cond1) and use it in place of the second (in cond2) but that’s a nono…

Any clues / elegant Elixir patterns for handling this kind of situations, rather than rewriting it into nested ifs/cases?

Hi,

it’s hard to say: it depends on the others conditions. You could use a with statement, rely on pattern matching by introducing an intermediate function or combine both.

For example:

defp handle_an_expensive_function_returning_b_result(result, a, _c) when result !== a, do: cond1_error()
defp handle_an_expensive_function_returning_b_result(result, _a, c) when c < result, do: cond2_error()
defp handle_an_expensive_function_returning_b_result(_result, _a, _c), do: no_errors()

if x !== a_non_expensive_function() do
  cond0_error()
else
  an_expensive_function_returning_b()
  |> handle_an_expensive_function_returning_b_result(a, c)
end
2 Likes

I think you’ll find the with keyword helpful: Control Structures · Elixir School

Something like (warning poor pseudocode ahead):

with {:ok, x} <- inexpensive_function()
        {:ok, b_result} <- an_expensive_function_returning_b()
do
    if c < b_result, do: cond2_error(), else: no_errors()
else
    {:error, msg} ->
       # handle errors
2 Likes

Since your return value of an_expensive_function_returning_b() must be the same as a to reach the third condition, couldn’t you just do:

cond do
	x !== a_non_expensive_function() -> cond0_error()
	a !== an_expensive_function_returning_b() -> cond1_error()
	c < a -> cond2_error()
	[…]
	true -> no_errors()
end

Maybe You could calculate the expensive function outside of the cond block… and set a variable.

That would require either the “anti-pattern” as the language creator finds it or additional glue code, which would normalise outputs of numerous functions to a common error struct for example. Possible, “but”.

I apologise for not mentioning that the example was a “dummy” code. In this particular case this would work but in reality the conditions can be various and differ from what’s in the example so it has to be “generic” approach.

Yes, I wrote:

For a more specific example: I don’t want to read through megabytes of a file in order to parse some stuff out of it unless I know it is worth doing this by first checking e. g. File.stat to make sure it is not gigabytes. If I read it before the block I might not like the outcome in case it is actually too large to handle :wink: So yes, I can rewrite the cond into some nested ifs/cases and it will work but the elegant terseness of the cond block would be gone.

This is a confusing chain of error conditions. Can we assume x, a, and c are bound before the cond call? Personally I would prefer to use cond for truthy comparisons. Can this be expressed as a with statement with an else clause instead, perhaps with a few helper functions to pattern match on the conditions? Maybe it’s just a matter of personal taste…

How do you feel about the process dictionary?

defp b_cache_wrapper do
  case Process.get(:an_expensive_function_returning_b_cache) do
    nil ->
      result = an_expensive_function_returning_b()
      Process.put(:an_expensive_function_returning_b_cache, result)
      result

    cached_value ->
      cached_value
  end
end

...

cond do
  x !== a_non_expensive_function() -> cond0_error()
  a !== b_cache_wrapper() -> cond1_error()
  c < b_cache_wrapper() -> cond2_error()
  […]
  true -> no_errors()
end
|> then(fn x -> Process.delete(:an_expensive_function_returning_b_cache); x end)
4 Likes

If your method calls are able to return ok/error tuples, you can side-step the anti-pattern issue :slight_smile:

1 Like
def compare(a, c, x) do
  with :ok <- a_non_expensive_function_comparison(x),
       :ok <- an_expensive_function_comparison(a, c) do
    no_errors()
  end
end

defp a_non_expensive_function_comparison(x) do
  if x === a_non_expensive_function() do
    :ok
  else
    cond0_error()
  end
end

defp an_expensive_function_comparison(a, c) do
  b = an_expensive_function_returning_b()

  cond do
    a !== b -> cond1_error()
    c < b -> cond2_error()
    true -> :ok
  end
end
2 Likes

Specific to avoid multiple expensive call for a function, you could implement caching based on function and argument (via one of in memory ETS based cache) with some very short TTL. I have found this kind of technique helpful in more complicated case (e.g nested function call / different call stack).

How do you feel about the process dictionary?

Ah, cache based on process dictionary is interesting! it would be good enough in this case, though it probably not able to handle spawning child task

with a <- a_non_expensive_function(),
     {:ok, ^a} <- if a != x, do: {:ok, a}, else: {:error, cond0_error()},
     b <- an_expensive_function_returning_b(),
     {:ok, ^b} <- if a != b, do: {:ok, b}, else: {:error, cond1_error()},
     {:ok, ^c} <- if c >= a, do: {:ok, c}, else: {:error, cond2_error()} do
       no_errors()
     else
       {:error, error} -> error
     end
end

I think this could work:

with true <- x != a_non_expensive_function() || cond0_error(),
     b = an_expensive_function_returning_b(),
     true <- a != b || cond1_error(),
     true <- c < an_expensive_function_returning_b() || cond2_error() do
  no_errors()
end

But I’d go with a nested condition personally.

1 Like

This would until cond0_error/0 returns true. Avoiding such hard-to-catch bugs is the main reason for using {:ok, _} and {:error, _} tuples instead of raw values.

Guys, first thank you for so many responses and edifying ideas! It’s really enlightening to read through them. I respond to some here

The one thing I am not so happy about in this approach is that it “disperses” the flow. If one looks at else if/elsif/elif etc. in other languages it is clear what the steps are. Similar can be achieved in Elixir with cond and with but the limitations are more strict here. I’d prefer to keep this one-look flow overview intact.

iWow!™ - that looks geeky enough to elevate my heartbeat rate :slight_smile: and it allows wrapping only the expensive call(s). Looks like a good “solution candidate” to me!

Right, but no, they are heterogenous and returning whatever pleases them. As for the anti-pattern in some particular cases, I wouldn’t care that much (something I expressed here) yet here this doesn’t apply, and adding annotations or whatever to make it work smells from half a mile away :wink: Wrapping all things and normalising returns would work well but this we know.

True. Although it looks like a bit of an overkill for the case at hand, doesn’t it?

O! That’s an interesting “abuse” of the with construct. For the current case it should in fact work as none of the errors evaluates to true. OTOH as mentioned already it can be brittle in the longer run. And you yourself mentioned you wouldn’t prefer it over nested constructs. Thanks for the enlightenment regardless!

I’m not sure how you’d be able to handle that in any other language without nesting or dispersing, but also not calling the expensive function unless actually needed.

One can argue that multiple else if is e. g. a syntax sugar over deeply nested if else so in such case you wouldn’t be able w/o nesting indeed.

But if taking the “sugared” syntax as “not nested” I would be able to declare a variable upfront (or not upfront - depending on the language) and assign to it the output of the expensive call on the first use, later using only the variable while still having everything in one place.

@t0t0’s answer would be my choice. Break the non-expensive away from the expensive stuff and handle each on its own.

3 Likes

True, but it wouldn’t be that much effort because you could easily make a generic wrapper for caching which accept arbitrary function name and argument, this is because ETS is able to accept (seems?) all erlang term as part of it’s dictionary key value

e.g something like this

def cache_an_expensive_function_returning_b(args) do
    CacheWrapper.wrap_caching({:an_expensive_function_returning_b, args}, fn -> an_expensive_function_returning_b(args) end)
end
1 Like

One of the nice thing about caching based solution is that generally you don’t need to think much about it / refactor much your existing code. For example if you function have multiple function path, and only in some (multiple) select function path you need to call you expensive function. you don’t need to refactor the code to ensure expensive function is called on once (and only if it’s needed)

2 Likes