Filtering Atoms with Nil Value

Problem Description:

Given a list of atoms like this [:a, :b, :c, a: 2], I need to add 1 to each atom that doesn’t have a value but keep the atoms that do. So, ideally, the list will end up being [a: 1, b: 1, c: 1, a: 2] which will then get put into a map.

What I’ve done so far:

The initial problem was adding atoms ([:pink, :purple, :pink, :red]) which can be accomplished pretty easily with Enum.frequencies() or with way more complexity by doing:

 def total(list) do
    list
    |> Enum.map(fn x -> {x, 1} end)
    |> Enum.group_by(fn {key, _value}-> key end, fn {_key, value}-> value end)
    |> Enum.map( fn {group, value_list} -> {group, Enum.reduce(value_list, 0, fn value, acc -> acc + value end)} end)
    |> Enum.into(%{})
  end

But I can’t figure out how to handle the atoms with no value when there are any other values in the list. I’m sure this is a simple when or unless statement, but I cannot seem to get the syntax down to make it handle atoms that have no value.

1 Like

Some things to consider, [:a, :b, :c, a: 2] is not just a list of atoms, this is in fact a list with some atoms and a keyword list at the end, the a:2 being without the tuple notation is just syntax sugar because is the last element in the list.

You can try this:

[:d, :e, f: 1, :g] and you will get an error of this kind:

“unexpected expression after keyword list. Keyword lists must always come last in lists and maps …”

So if you want to handle a list that can mix just atoms and the keyword list, you should better work with the tuple representation:

[:d, :e, {:f, 1}, :g]

With that in mind you can do something like this:

Enum.map(list, fn
   {_, _} = kw -> kw
   a when is_atom(a) -> {a, 1}
end)
3 Likes

Here are my few cents …

If you are familiar with operators you can write something like a && b || c. First let’s describe the order. It’s really simple to check it:

iex> quote do   
...>   1 && 2 || 3
...> end
{:||, [(…)],
 [{:&&, [(…), [1, 2]}, 3]}

Ok, so && operator have a bigger priority and the result of it is simply passed as a first argument to || operator.

We can use it to write a simple condition:

iex> func = fn term ->
...>  is_nil(term) && "it's nil" || "it's not nill"
...> end

iex> func.(nil)
"it's nil"
iex> func.("not nil")
"it's not nil"

What happen inside the function? We have a 3 simple steps:

  1. Firstly is_nil/1 check is called
  2. Secondly && (boolean and operator) evaluates and returns the second expression only if the first one (is_nil/1 check) evaluates to true (i.e. truthy value). Returns the first expression otherwise (here: false i.e. falsy value).
  3. Finally || (boolean or operator) evaluates and returns the second expression only if the first one (result of && operator) does not evaluate to a truthy value (here: false). Returns the first expression (here: second expression of && operator) otherwise.

Great! Now let’s use it in Enum.map/2 call with a & (capture operator):

iex> list = [:a, :b, :c, a: 2]
[:a, :b, :c, a: 2]
iex> Enum.map(list, &(is_atom(&1) && {&1, 1} || &1)) 
[a: 1, b: 1, c: 1, a: 2]

In that case we do not need to write 2 pattern matching expressions inside a Enum.map/2 mapper function, but that’s not everything … Your total/1 function is not really efficient. You can even use a single Enum.reduce/3 call!

defmodule Example do
  def original(list) do
    list
    |> Enum.map(&((is_atom(&1) && {&1, 1}) || &1))
    |> Enum.group_by(fn {key, _value} -> key end, fn {_key, value} -> value end)
    |> Enum.map(fn {group, value_list} ->
      {group, Enum.reduce(value_list, 0, fn value, acc -> acc + value end)}
    end)
    |> Enum.into(%{})
  end

  def new(list) do
    Enum.reduce(list, %{}, fn element, acc ->
      {key, value} = (is_atom(element) && {element, 1}) || element
      Map.update(acc, key, value, &(&1 + value))
    end)
  end
end

iex> list = [:a, :b, :c, a: 2]
[:a, :b, :c, a: 2]
iex> Example.original(list) == Example.new(list)
true

That’s just one iteration over one list with only 4 lines of code! What happens here?

Let’s give it a check:

  1. First of all we are reducing an existing list over an empty map. Since map is our desired result we can simply use the return value of Enum.reduce/3 function without any extra transformations.
  2. In second line of function body we determine both key and a default or extra value we want to add (depending if said key is already in map or not).
  3. Finally we call a Map.update/4 function which puts a default value if key does not exists or adds extra value if key already exists into our acc (a map accumulator).

Helpful resources:

  1. Operators
  2. && (boolean and operator)
  3. || (boolean or operator)
  4. Kernel.SpecialForms.quote/2
  5. & (capture operator)
  6. Enum.map/2
  7. Enum.reduce/3
  8. Map.update/4
3 Likes

Could also look here: Operators reference — Elixir v1.17.3

2 Likes

Thanks, I have updated my Helpful resources section