Reputation: 5472
Suppose I have a map in Elixir:
m = %{"a"=>1, "b"=>2, "c" => 3}
If I call Map.values(m)
, am I guaranteed that the return value will always be [1, 2, 3]
in that order and not say, [3, 1, 2]
?
This is one thing I am not clear on from the docs. After some preliminary testing, I think it is.
Upvotes: 25
Views: 13264
Reputation: 2399
The iteration order for maps is not just "undefined" in the sense that:
The second property is true for lists:
Enum.zip_with(1..32, 1..32, fn a, b -> {a, b} end)
# [{1, 1}, {2, 2}, {3, 3}, ...]
... but is not true for Maps with more than 32 entries:
range = 1..33
map = Enum.map(range, &{&1, true}) |> Enum.into(%{})
IO.inspect(Enum.map(map, fn {k, _v} -> k end))
IO.inspect(Enum.zip_with(range, map, fn _, {k, _v} -> k end))
# [4, 25, 8, 1, 23, 10, 7, 9, 11, 12, 28, 24, 13, 3, 18, 29, 26, 22, 19, 2, 33, 21, 32, 20, 17, 30, 14, 5, 6, 27, 16, 31, 15]
# [15, 31, 16, 27, 6, 5, 14, 30, 17, 20, 32, 21, 33, 2, 19, 22, 26, 29, 18, 3, 13, 24, 28, 12, 11, 9, 7, 10, 23, 1, 8, 25, 4]
It looks like Enum.zip_with
enumerates over the entries of a Map in the opposite order from Enum.map
.
Upvotes: 0
Reputation: 248
As of OTP 26, May 2023, maps with atom keys will have an undefined order.
In OTP 26, as an optimization for certain map operations, such as maps:from_list/1, maps with atom keys are now sorted in a different order. The new order is undefined and may change between different invocations of the Erlang VM
fly.io have a blog post that some might be useful for sorting maps.
Upvotes: 1
Reputation: 75840
From the Elixir website:
Compared to keyword lists, we can already see two differences:
- Maps allow any value as a key.
- Maps’ keys do not follow any ordering.
While the Elixir website clearly states that Maps do not follow any ordering, they do follow a specific order after they've created (but do not preserve their order of creation). It seems that the Maps are organized alphabetically according to their keys (but I have nothing to back this up except a few experiments in IEx):
map = %{c: 3, a: 1, b: 2}
map # => %{a: 1, b: 2, c: 3}
Map.keys(map) # => [:a, :b, :c]
Map.values(map) # => [1, 2, 3]
Since you asked about preserving the original order, The answer is NO.
A better alternative would be to use Keyword lists (which are a linked-list of two element tuples underneath). Because of this, the order of their creation is maintained:
kw = [c: 3, a: 1, b: 2]
kw # => [c: 3, a: 1, b: 2]
Keyword.keys(kw) # => [:c, :a, :b]
Keyword.values(kw) # => [3, 1, 2]
Upvotes: 14
Reputation: 2863
Let's check the Erlang's code.
Elixir use Erlang's map as base implementation,it defines a MAP_SMALL_MAP_LIMIT macro.
erts/emulator/beam/erl_map.h-#ifdef DEBUG
erts/emulator/beam/erl_map.h:#define MAP_SMALL_MAP_LIMIT (3)
erts/emulator/beam/erl_map.h-#else
erts/emulator/beam/erl_map.h:#define MAP_SMALL_MAP_LIMIT (32)
erts/emulator/beam/erl_map.h-#endif
When you create a map, it will use the function below.
Eterm erts_map_from_ks_and_vs(ErtsHeapFactory *factory, Eterm *ks0, Eterm *vs0, Uint n)
{
if (n < MAP_SMALL_MAP_LIMIT) {
Eterm *ks, *vs, *hp;
flatmap_t *mp;
Eterm keys;
hp = erts_produce_heap(factory, 3 + 1 + (2 * n), 0);
keys = make_tuple(hp);
*hp++ = make_arityval(n);
ks = hp;
hp += n;
mp = (flatmap_t*)hp;
hp += MAP_HEADER_FLATMAP_SZ;
vs = hp;
mp->thing_word = MAP_HEADER_FLATMAP;
mp->size = n;
mp->keys = keys;
sys_memcpy(ks, ks0, n * sizeof(Eterm));
sys_memcpy(vs, vs0, n * sizeof(Eterm));
erts_validate_and_sort_flatmap(mp);
return make_flatmap(mp);
} else {
return erts_hashmap_from_ks_and_vs(factory, ks0, vs0, n);
}
return THE_NON_VALUE;
}
Notice that when the size of the map is less than MAP_SMALL_MAP_LIMIT
, it will call erts_validate_and_sort_flatmap
, which use bubble sort to sort your map.
Otherwise, it will use hashmap.
Upvotes: 4
Reputation: 9109
The implementation of Maps in Elixir and Erlang has some confusing properties. For small values of entries it is a sorted key list, and thus appears to have the properties you see in simple experiments.
Above a certain number of entries (32 I think), the implementation switches to Hash Array Mapped Trie and all the properties you see disappear. You can not depend on the order of either the keys or the values of a map in the general case. See
https://en.wikipedia.org/wiki/Hash_array_mapped_trie
for an explaination of the underlying structure of Map.
iex(7)> 1..33 |> Enum.reduce(%{}, fn(x, acc) -> Map.put(acc,x,x) end )
%{11 => 11, 26 => 26, 15 => 15, 20 => 20, 17 => 17, 25 => 25, 13 => 13, 8 => 8, 7 => 7, 1 => 1, 32 => 32, 3 => 3, 6 => 6, 2 => 2, 33 => 33, 10 => 10, 9 => 9, 19 => 19, 14 => 14, 5 => 5, 18 => 18, 31 => 31, 22 => 22, 29 => 29, 21 => 21, 27 => 27, 24 => 24, 30 => 30, 23 => 23, 28 => 28, 16 => 16, 4 => 4, 12 => 12}
iex(8)> Map.keys(v(7)) [11, 26, 15, 20, 17, 25, 13, 8, 7, 1, 32, 3, 6, 2, 33, 10, 9, 19, 14, 5, 18, 31, 22, 29, 21, 27, 24, 30, 23, 28, 16, 4, 12]
iex(9)> Map.values(v(7)) [11, 26, 15, 20, 17, 25, 13, 8, 7, 1, 32, 3, 6, 2, 33, 10, 9, 19, 14, 5, 18, 31, 22, 29, 21, 27, 24, 30, 23, 28, 16, 4, 12]
Upvotes: 42