Right leg
Right leg

Reputation: 16710

Break a comprehension

Is there a way to interrupt a comprehension?

I'm thinking to something like Python's for-break-else. For instance, suppose I'm traversing a list of integers, and that while I'm generating a new list out of it, I want to check that it does not contain 0.

Can I do something like this:

for x <- [1, 2, 0, 3] do
    case x do
        0 -> break
        _ -> x
    end
end
if broke do
    :error
else
    :ok
end

I guess I'm thinking too imperative, and that the correct functional approach would be something like this, with a recursion and an accumulator:

def traverse_and_check(list, acc \\ []) do
    case list do
        [] -> {:ok, reverse(acc)}
        [0 | _tail] -> :error
        [x | tail] -> traverse_and_check(tail, [x | acc])
    end
end

Does Elixir have such a flow of control?

Upvotes: 3

Views: 5443

Answers (3)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

If you are not interested in returning the collected part of the list, Elixir provides throw/catch exactly for that purpose:

In Elixir, a value can be thrown and later be caught. throw and catch are reserved for situations where it is not possible to retrieve a value unless by using throw and catch.

Those situations are quite uncommon in practice except when interfacing with libraries that do not provide a proper API.

I would consider this situation being the perfect example of “libraries [here—Elixir] that do not provide a proper API” :)

So here you go:

try do
  for x <- [1, 2, 0, 3],
    do: if x == 0, do: throw(:break), else: x
catch
  :break -> :broken
end
#⇒ :broken

Please note, throw/catch are intended to control the flow, unlike raise/rescue.

Upvotes: 11

Dogbert
Dogbert

Reputation: 222118

You can use Enum.reduce_while/3 but the solution is worse than recursion in my opinion since it requires a case afterwords to reduce the list for the non breaking case. The snippet below includes the reduce_while implementation and an improved recursive solution based on your code:

defmodule A do
  def f(list) do
    Enum.reduce_while(list, [], fn x, acc ->
      if x == 0, do: {:halt, :break}, else: {:cont, [x | acc]}
    end)
    |> case do
      :break -> :break
      list -> Enum.reverse(list)
    end
  end

  def g(list, acc \\ [])

  def g([], acc), do: Enum.reverse(acc)
  def g([0 | _], _), do: :break
  def g([x | xs], acc), do: g(xs, [x | acc])
end

IO.inspect(A.f([1, 2, 3]))
IO.inspect(A.f([1, 2, 0, 3]))
IO.inspect(A.g([1, 2, 3]))
IO.inspect(A.g([1, 2, 0, 3]))

Output:

[1, 2, 3]
:break
[1, 2, 3]
:break

Upvotes: 2

Ben Harris
Ben Harris

Reputation: 557

Have a look at the comprehensions page of the getting-started manual.

Comprehensions have optional filters that you can use in place of the standard filter/reduce functions. Try something like this:

for x <- [1, 2, 0, 3], x != 0, into: [] do: x

The documentation has some better examples, but this should do for your use case of removing zeros from a list in a comprehension.

Upvotes: 0

Related Questions