How to avoid using Enumerable.impl_for

Hi all,
I have the following function to count nested map size. It works by counting number of keys in the current map and adding it to the map size of each of its values.

defmodule Counter do
  def nested_map_size(m) when is_map(m) do
    current = m |> Map.keys |> Enum.count
    case Enumerable.impl_for m do
      nil -> current
      _other -> Enum.reduce(m, current, fn {_k, v}, acc -> acc + nested_map_size(v) end )
    end
  end
  def nested_map_size(_), do: 0
end

I added the case clause because I found that when I receive structs they pass the is_map guard but do not implement the Enumerable protocol which causes an exception on the reduce function,
I was asking myself - is that the best practice here? Is there a better way to handle the difference in behaviors of map and structs even though they are both considered maps underneath?
Thanks in advance.

You can use functions from :maps module, probably. Like :maps.size/1 and :maps.fold/3 and avoid Enum completely.

defmodule Counter do
  def nested_map_size(m) when is_map(m) do
    :maps.fold(fn _k, v, acc -> acc + nested_map_size(v) end, :maps.size(m), m)
  end
  def nested_map_size(_), do: 0
end
1 Like

I like your suggestion I wasn’t aware of the :maps module.
Is it part of elixir or erlang? I wonder if there’s an Elixir equivalent

It’s erlang’s. There is also elixir’s Map which wraps some of :maps but it lacks both of the functions that I’ve used above (there is Map.size/1 though, but it’s been deprecated).

One way to separate maps and structs could be to do something like:

defmodule Counter do
  def nested_map_size(%_struct{}), do: 0
  def nested_map_size(%{} = m) do
    Enum.reduce(m, map_size(m), fn {_k, v}, acc -> acc + nested_map_size(v) end)
  end
  def nested_map_size(_), do: 0
end

In general the elixir equivalent to :maps.size/1 would be Kernel.map_size/1.

2 Likes

Thanks for the alternative.

Just out of curiosity: what does %_struct{} match all structs? I mean I know structs have the __struct__ key in the underlying map but I wasn’t aware of this matching rule.

I also must say that it would make sense to allow to guard functions by protocols rather than specific types.
For example, I’d like to guard by is_enumerable and not by is_map, is_list etc…

I wonder - why these kinds of guards aren’t available?

1 Like

I think @micmus meant %{__struct__: _} which will match on all maps with the __struct__ key. This destructuring is, in many ways, more powerful than a guard. Anyway, they are complementary - the pattern matching being perhaps more powerful but with guards to add additional checks. So…

defmodule Counter do
  def nested_map_size(%{__struct__: _}), do: 0
  def nested_map_size(%{} = m) do
    Enum.reduce(m, map_size(m), fn {_k, v}, acc -> acc + nested_map_size(v) end)
  end
  def nested_map_size(_), do: 0
end

I doubt it. Just as you can match func(%SomeStruct{}) do […] you can match multiple structs func(%struct{}) when struct in [SomeStruct, SomeOtherStruct] do […]. Now if you want to match only structs, without binding the struct name to a variable you’d do func(%_{}) do […] or func(%_struct{}) do […]. This way you don’t need to rely on the implementation detail that a struct is defined by the __struct__ key in the map, which – as unlikely as it is – may change.

5 Likes

And … I learn something new every day! I did not know that! Even thought the logic is very sound. I always assumed that the %___{} construct was a compile time thing. My bad. And they way you describe it makes it clear how it works, thanks.

1 Like

This was introduced fairly recently, I think maybe in 1.3 or 1.4, I don’t remember any more. But yes, that’s the feature I was leveraging. That said, I don’t think __struct__ will change and it’s entirely safe to use it as well.

Thanks all for your answers I guess the main point here is that the struct name may be pattern matched as any other value in the map.
Any opinions on guards that filter protocols rather than specific types?

It is possible via a macro that tests a protocol to get all implementations and match based on that, but no one has done it yet.

/me has an idea to add to their ProtocolEx library…