Extract values from the map, and make a counter for them

Hello.
I want to iterate over the collection (array) where each element is a map containing key-value pairs. In there, I am interested in a key called like_type that can be true or false. What I want is to make a counter of how many true and false atoms are within this array.
I have made this function that should work in Javascript (haha), and I need help translating this into Elixir.

def get_number_of_votes(all_likes) do
    upvote_count = 0
    downvote_count = 0

    for like <- all_likes do
      case like.like_type do
        true ->
          upvote_count = upvote_count + 1
        false ->
          downvote_count = ^downvote_count + 1
        _ ->
          IO.puts("No true or false here.")
      end
    end
    res = %{upvote_count: upvote_count, downvote_count: downvote_count}
    IO.inspect(res)
    res
  end

Issue here is that I get this error:

“show_component.ex:13: cannot use ^downvote_count outside of match clauses”

This is what I get rendered within my liveview in Phoenix framework.

Thank you.

Here is a small example:

defmodule MyTest do
  def main() do
    list = [%{like_type: true}, %{like_type: false}, %{like_type: true, key2: :asdf}, %{}]
    {true_count, false_count} = count_like_type(list, 0, 0)
    IO.puts true_count
    IO.puts false_count
  end

  def count_like_type([], true_count, false_count), do: {true_count, false_count}
  def count_like_type([%{like_type: true} | rest], true_count, false_count) do
    count_like_type(rest, true_count + 1, false_count)
  end
  def count_like_type([%{like_type: false} | rest], true_count, false_count) do
    count_like_type(rest, true_count, false_count + 1)
  end
  def count_like_type([_head | rest], true_count, false_count) do
    count_like_type(rest, true_count, false_count)
  end
end

MyTest.main()

Which outputs:

> mix run lib/my_test.exs
2
1

Pattern matching map elements in the function head + recursion is fun and useful!

It is possible to use Enum.reduce, too. Your example wasn’t using standard modules so this one doesn’t either.

2 Likes

Thank you for the response!
Seems like the way we do this type of problem in Elixir is to flirt with Enum structures. Lots of ceremony there. I need to investigate more about the reduce method because it seems that it does the job elegantly.

1 Like
all_likes
|> Enum.filter(&Map.has_key?(&1, :like_type))
|> Enum.reduce(%{upvote_count: 0, downvote_count: 0}, fn %{like_type: like_type}, acc ->
 if like_type do
   Map.put(acc, :upvote_count, acc.upvote_count + 1)
 else
   Map.put(acc, :downvote_count, acc.downvote_count + 1)
 end
end)

You’ll generally need to reach for reduce when you’re iterating over a list or map. It can be thought of as iterating over each element and condensing the elements down to a single value which is returned as the result of the reduce operation. In this case, it is a map with the upvote and downvote counts

3 Likes

Two initial thoughts:

  • what were you hoping to happen when you wrote ^downvote_count?
  • writing some_existing_variable = "new_value" inside a block does not do what you want - it rebinds the name inside the block, but does not change the original binding outside

If you’re using Enum functions, a short way to write this is just like you’d describe the process manually:

all_likes
|> Enum.split_with(& &1.like_type)
|> then(fn {upvotes, downvotes} -> %{upvote_count: length(upvotes), downvote_count: length(downvotes)} end)

In words, this takes all_likes, divides it into two lists based on like_type and then summarizes the results with length.

4 Likes

Nice use of split_with - Pretty slick

1 Like

Yes. This seems like the way Elixir wants me to do this problem. Pipes and filters and reducers. It does the job and its more elegant. Thank you for the response! JohnnyCurran

1 Like

Thanks for the reponse! @ al2o3cr
With : ^downvote_count I was hoping that I overwrite the variable I have declared in the outer scope. Basically, I was experimenting and left it out there. split_with is an interesting function, one more in my Elixir vocabulary!

This will throw an error if the key isn’t in map. Might not be in the use-case either way though.

You can make use of Enum.frequencies_by/2, which will return a map that you can then extract the true and false counts to put into a new map as the upvoted and downvoted counts:

frequencies = Enum.frequencies(all_likes, & &1[:like_type])
%{upvoted_count: frequencies[true] || 0, downvoted_count: frequencies[false] || 0}
1 Like