Another way to answer your question is The Robustness Principle.
Putting at
and reverse
in Enum allows them to be as liberal as possible by accepting any Enum. And, if want to take advantage of the methods in Enum
you need only implement the Enum
protocol for your data type. This allows your custom data type to feel “ergonomic” and “familiar” to the average Elixir dev, kind of neat, no?
Re: at
not making sense for Map
s. I disagree.
I think the issue here is that the docs for Map
s state that their ordering is “arbitrary,” without an explanation of what that means. From what I can tell, the ordering is determined by erts_internal:map_next
(zhttps://github.com/erlang/otp/blob/OTP-27.0.1/lib/stdlib/src/maps.erl#L537), and I don’t know Erlang so it will be a minute before I can explain it. But, arbitrary doesn’t mean “random.” Nor does mean “possibly different between invocations.” So what does “arbitrary” mean to communicate here? I can see two possibilities: the definition for map_next
could change between erlang versions. This would mean that between an upgrade one map could return different results for Enum.at(m, n)
for some n
. Secondly, the use of “arbitrary” may be to convey that small changes to map (such as adding a new key) may have large, unexpected, changes to the ordering. E.g., Given a map m
and a number n
with n < Map.size(m)
when Enum.at(m, n) # returns x
you may expect that
m2 = Map.put(m, :new_key, :some_value)
Enum.at(m2, n) # returns x
Which may not be the case. The ordering, as they say, is arbitrary, so there is no promise of consistent ordering among “similar” maps.
Re: why isn’t insert_at
or delete_at
in Enum?
Your example illustrates that some types, which are Enumerable
, can’t be maintained from these two “write” operations. E.g., you provided an example of an insert_at
on a Range which produces a List. That list, can’t be expressed as a Range. Similarly for your delete_at
example. I think this reason enough is to exclude these methods from Enum
, though my opinion is subject to change on that. The bigger issue is that, as Jose said, not all types which are Enumerable are “obvious” for how an insert/delete should occur. For example, we can find a way to make a Map enumerable (barring the fact that to you it feels unintuitve at the moment), but what would the expected outcome of a Enum.insert_at(map, 2, {:key, :value})
even be? Is it the same or different from Enum.insert_at(map, 3, {:key, :value})
? And how do you make sure _that_ choice is consistent with the behavior of
Map.to_list? if you insert something at index
n` it had better be at that same index when you convert it to a list, no?
Edit: I hate to go on beating a dead horse, but I also want to address this comment in your initial post,
Map
s key/value pairs are not ordered.
There is some trickery afoot with language when we say something is ordered. Enumerable doesn’t mean ordered, it means orderable. More specifically it means “this is a thing that contains a bunch of other things and we have a function that can assign each of those things to a natural number.” So yes, maps are not ordered, but enums don’t need to be ordered, they need to be orderable.
Edit 2: I’m not sure that my post helped clear any of your confusions. I hope it did. Or at least I hope it convinced you that the language design choice on the part of Elixir is sane in this case. But if it didn’t, note that this also exists for other languages as well, see ElementAt for IEnumerable in C#. Dictionaries in .NET (aka maps) are IEnumerable. Or HashMaps in Rust, which implement the IntoIterator trait. In fact, the docs for the .iter()
method in Rust which converts the HashMap into an Iterator uses the same phrasing (“arbitrary order”) as Elixir.
Edit 3: I realized this morning that Enum.drop(range, n)
also returns a list, so ignore that part of the rest of this post. I don’t know if I can think of a good reason for delete_at
to not exist in Enum. it would essentially just be Enum.with_index() |> Enum.reject(fn {_, i} -> i != n end) |> Enum.map(&(elem(&1,0))