6
votes

In a case statement in Elixir, is it possible to do nothing if a particular condition is met in a case statement? Or must something always be returned?

To illustrate, here's a snippet from a Phoenix app that I'm working on:

Enum.map(record_params, fn(record_id) ->
    record = Repo.get!(Record, record_id)
    case Repo.update(record) do
        {:ok, struct} ->
            # I DON'T REALLY NEED ANYTHING TO HAPPEN HERE... BUT I HAVE TO HAVE A CLAUSE TO MATCH WHEN THE UPDATE RETURNS {:ok, struct}
            IO.inspect struct
        {:error, changeset} ->
            errors = parse_errors(changeset)
            IO.inspect errors
            json(conn |> put_status(400), %{status: "error", message: "There was a problem updating this record.", errors: errors})
                end
end)

If the record is updated, I need to know if there's an error, get information about it, and return that to the client, hence the need for the case statement... but I don't really need to do anything if the record has been updated successfully - {:ok, struct}. Since these updates are taking place inside an Enum.map(), if the update is successful, I just want the map to continue to loop through the record_ids.

For the time being, I've just been putting IO.inspect struct in the success condition - this is harmless, but not really necessary. I'd prefer to clean up my code, if possible. I can't remove the {:ok, struct} condition due to Elixir's pattern matching, and if I put nothing at all under that condition, I get the error syntax error before: '->'.

Now I am totally new to Elixir (and the functional programming paradigm, in general), so if there is a more 'Elixirish' way to handle this sort of scenario, I would love to hear about it.

5
Does this even work? You're potentially calling json multiple times here. Also, how are you handling the response for when all updates succeed? - Dogbert
You're right, this is problematic. This success response comes later on, after some other code executes - I left it out because it didn't seem germane. - skwidbreth

5 Answers

19
votes

Despite there is already a bunch of answers, I would post another one. The idiomatic Elixir way of ignoring all but one return would be to use Kernel.SpecialForms.with/1:

with {:error, changeset} <- Repo.update(record) do
  # Do whatever you want here
end

The do block would be executed if and only the match happened, otherwise the non-matched RHO value ({:ok, _} would be directly returned.)

For the correct code performing what you wanted, please refer to @Dogbert’s answer.

3
votes

The Erlang convention inherited by Elixir is to just use :ok as with the output of IO.puts

There is no allocation overhead returning an atom, since they are all interned by the runtime.

3
votes

Your code is wrong because you're potentially calling json multiple times on the same conn in case there are multiple Repo.update errors. You need to stop processing the rest of the list in that case. Also, since you're not actually detecting when there are any errors, you're probably unconditionally calling json again later on the same conn. If you look at your logs, you should see that Phoenix is writing a response to the same conn multiple times, e.g.

[debug] Processing by MyApp.PageController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 130µs
[info] Sent 200 in 232µs
[info] Sent 200 in 498µs
[info] Sent 200 in 390µs
[info] Sent 200 in 182µs

Here's how I'd do this with Enum.reduce_while/3:

errors = Enum.reduce_while(record_params, nil, fn(record_id, nil) ->
  record = Repo.get!(Record, record_id)
  case Repo.update(record) do
    {:ok, struct} ->
      {:cont, nil}
    {:error, changeset} ->
      errors = parse_errors(changeset)
      {:halt, errors}
  end
end)

if errors do
  json(conn |> put_status(400), %{status: "error", message: "There was a problem updating this record.", errors: errors})
else
  json(conn, "no errors!")
end
2
votes

Every Elixir function returns the value of the last evaluated statement. If you don't want to return anything from a function whose return value won't be used anyway, you can just add nil to cases where nothing needs to be returned.

Enum.map(record_params, fn(record_id) ->
    record = Repo.get!(Record, record_id)
    case Repo.update(record) do
        {:ok, struct} ->
            nil
        {:error, changeset} ->
            errors = parse_errors(changeset)
            IO.inspect errors
            json(conn |> put_status(400), %{status: "error", message: "There was a problem updating this record.", errors: errors})
                end
end)
1
votes

In this particular case, because you are only wanting to do something for one branch, you could just use if/2.

{status, changeset} = Repo.update(record)
if status == :error do
  # Do whatever you want here
end

If you need to match on multiple things, you would want to use the case and just return something for that branch. In your case above, I would return :ok, because the action was successful.