That's just the way Ruby's collection framework works. There is one map
method in Enumerable
which doesn't know anything about hashes or arrays or lists or sets or trees or streams or whatever else you may come up with. All it knows is that there is a method named each
which will yield
one single element per iteration. That's it.
Note that this is the same way the collections frameworks of Java and .NET work, too. All collections operations always return the same type: in .NET, that's IEnumerable
, in Ruby, that's Array
.
Another design approach is that collections operations are type-preserving, i.e. mapping a set will produce a set, etc. That's the way it is done in Smalltalk, for example. However, in Smalltalk, but there it is achieved by copy&pasting almost identical methods into each and every different collection. I.e. if you want to implement your own collection, in Ruby, you only have to implement each
, and you get everything else for free, whereas in Smalltalk, you have to implement every single collection method separately. (In Ruby, that would be over 40 methods.)
Scala is the first language that managed to provide a collections framework with type-preserving operations without code duplication, but it took until Scala 2.8 (released in 2010) to figure that out. (The key is the idea of collection builders.) Ruby's collections library was designed in 1993, 17 years before we had figured out how to do type-preserving collections operations without code duplication. Plus, Scala depends heavily on its sophisticated static type system and type-level metaprogramming to find the correct collection builder at compile time. This is not necessary for the scheme to work, but having to look up the builder for every operation at runtime may incur a hefty runtime cost.
What you could do is add new methods that are not part of the standard Enumerable
protocol, for example similar to Scala's mapValues
and mapKeys
.