Tarlen
Tarlen

Reputation: 3797

Scoping problems with comprehensions

I want to perform some analytics on a series of combinations of values.

I have the following function, but for some reason, after the comprehensions are completed and at the end of the function body, the variable analytics is still an empty list, while it's not inside the comprehension on each iteration

Any ideas?

def handle_cast({:start}, state) do
    intervals = [7, 30, 90]
    groupings = ["day_of_week", "time_of_day"]
    aggregators = [
      %{
        domain: "support",
        metric: "new_conversations",
        func:   &App.get_new_conversations/2
      },
      %{
        domain: "support",
        metric: "closed_conversations",
        func:   &App.get_closed_conversations/2
      },
      %{
        domain: "support",
        metric: "median_response_time",
        func:   &App.get_median_response_time/2
      },
    ]

    Repo.transaction(fn ->
      Repo.delete_all(Analytic)

      analytics = []
      for interval <- intervals do
        for grouping <- groupings do
          for %{domain: domain, metric: metric, func: func} <- aggregators do
            analytic =
              func.(grouping, interval)
              |> Enum.map(fn %{"app_id" => app_id, "data" => data} = result ->
                %Analytic{app_id: app_id, domain: domain, metric: metric, grouping: grouping, interval_in_days: interval, data: data}
              end)

            analytics = [analytic|analytics]
          end
        end
      end
    end)

    {:noreply, state}
  end

Upvotes: 2

Views: 77

Answers (1)

Dogbert
Dogbert

Reputation: 222118

Variables in Elixir are immutable but rebindable. What this means is that the line analytics = [analytic|analytics] is creating a new list and binding it to the variable named analytics for the scope of that block. When the block ends, the changes are not persisted in the next iteration of the for. For example:

iex(1)> x = 1
1
iex(2)> for i <- 1..3 do
...(2)>   IO.puts(x); x = x + i; IO.puts(x)
...(2)> end
1
2
1
3
1
4

For the code you've written, you can use the fact that for returns a list of the values of the last expression inside them and store the return value of the outermost for to analytics, but there's a slight problem: you'll end up with nested lists:

iex(1)> for i <- 1..2 do
...(1)>   for j <- 1..2 do
...(1)>     for k <- 1..2 do
...(1)>       {i, j, k}
...(1)>     end
...(1)>   end
...(1)> end
[[[{1, 1, 1}, {1, 1, 2}], [{1, 2, 1}, {1, 2, 2}]],
 [[{2, 1, 1}, {2, 1, 2}], [{2, 2, 1}, {2, 2, 2}]]]

But, there's a simple solution! for accepts multiple <- clauses in a single call and automatically returns a flat list:

iex(1)> for i <- 1..2, j <- 1..2, k <- 1..2 do
...(1)>   {i, j, k}
...(1)> end
[{1, 1, 1}, {1, 1, 2}, {1, 2, 1}, {1, 2, 2}, {2, 1, 1}, {2, 1, 2}, {2, 2, 1},
 {2, 2, 2}]

Using this method, your code becomes:

analytics =
  for interval <- intervals,
      grouping <- groupings,
      %{domain: domain, metric: metric, func: func} <- aggregators do
      func.(grouping, interval)
      |> Enum.map(fn %{"app_id" => app_id, "data" => data} = result ->
        %Analytic{app_id: app_id, domain: domain, metric: metric, grouping: grouping, interval_in_days: interval, data: data}
      end)
    end
  end
end

This should give you the same output you were most likely expecting from the original code.

Upvotes: 3

Related Questions