Using `with`.. but the other way around

What is the most elegant way to do the following?

I have multiple functions (strategies) that I want to invoke, in order. Each can return {:ok, result} or {:error, :not_found}. If the first strategy fails the second one should be tried and so on. The return value should be that of the first succeeding strategy, or the return value (error tuple) of the last strategy if none succeed.

To give some context, I’m trying to locate the contact page on websites. First strategy is to try /contact, then /contact.html, then to look on the homepage for <a href="...">Contact</a>, and so on.

My current solution is something like this:

case find_contact_page_url1(website_url) do
  {:ok, contact_page_url} ->
    {:ok, contact_page_url}
  {:error, :not_found} ->
    case find_contact_page_url2(website_url) do
      {:ok, contact_page_url} ->
        {:ok, contact_page_url}
      {:error, :not_found} ->
        find_contact_page_url3(website_url)
  end
end

At first I thought this can be done using with but it does exactly the opposite (invoke multiple functions as long as each one matches).

1 Like

You can use with:

with {:error, :not_found} <- find_contact_page_url1(website_url),
     {:error, :not_found} <- find_contact_page_url2(website_url),
     {:error, :not_found} <- find_contact_page_url3(website_url) do
  {:error, :not_found}
else
  {:ok, contact_page_url} ->
    ...
end

Or, if the signature is always the same, use other functions such as Enum.find_value/2:

lookups = [&find_contact_page_url1/1, &find_contact_page_url2/1, &find_contact_page_url3/1]
Enum.find_value lookups, fn fun ->
  case fun.(website_url) do
    {:ok, value} -> value
    {:error, :not_found} -> nil
  end
end
11 Likes

Oh gotcha… clever, thanks! I like the first one better, it’s more explicit:)

1 Like