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
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 isMap.size/1 though, but it’s been deprecated).
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?
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.
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.
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?