Feature #7297

map_to alias for each_with_object

Added by Nathan Broadbent over 1 year ago. Updated over 1 year ago.

[ruby-core:48997]
Status:Rejected
Priority:Normal
Assignee:-
Category:lib
Target version:2.0.0

Description

I would love to have a shorter alias for 'eachwithobject', and would like to propose 'map_to'. Here are my arguments:

  • It reads logically and clearly:

[1, 2, 3].map_to({}) {|i, hash| hash[i] = i ** 2 }
#=> {1 => 1, 2 => 4, 3 => 9}

  • Rubyists are already using 'map' to build and return an array, so it should be obvious that 'map_to(object)' can be used to build and return an object.

  • Given that 'each' and 'eachwithindex' return the original array, I feel that the 'eachwithobject' method name is slightly counterintuitive. 'map_to' might not be 100% semantically correct, but it's obvious that it will return something other than the original array.

  • Many people (myself included) were using inject({}) {|hash, el| ... ; hash } instead of 'eachwithobject', partly because of ignorance, but also because 'eachwithobject' is so long. 'map_to' is the same length as inject, and means that you don't have to return the object at the end of the block.

  • Only a single line of code is needed to implement the alias.

Best,
Nathan

History

#1 Updated by Yukihiro Matsumoto over 1 year ago

  • Status changed from Open to Rejected

I feel the name #map_to does not make sense for the behavior.
If the behavior were like this,

def map_to(o)
self.each {|x|
o << x
}
o
end

at least makes sense. I know that's not what you want, so I reject this proposal.

Matz.

#2 Updated by Rodrigo Rosenfeld Rosas over 1 year ago

I just searched eachwithobject in GitHub and aside from some specs testing eachwithobject all real-use cases I could see after navigating though several pages of the results were ".eachwithobject({})...":

https://github.com/search?langOverride=&language=Ruby&q=each_with_object&repo=&start_value=15&type=Code

That is why I think we should have a .hash_map method instead of another generic one:

double = numbers.hash_map{|h, n| h[n] = n * 2 }

Better to read than

double = numbers.eachwithobject({}){|n, h| h[n] = n * 2 }

But other than that, I don't really think mapto is a bad name for the generic case (I'm replacing the arguments order of the block since it makes more sense to me instead of using it as an alias to eachwith_object):

double = numbers.map_to({}){|h, n| h[n] = n * 2 }

I read this as "map numbers to hash" which makes lots of sense to me. Much more than eachwithobject. Which only seems to be used with hashes by the way...

#3 Updated by Thomas Sawyer over 1 year ago

=begin
But the crux of the problem is simply that #eachwithobject has a name that it is too long, which greatly deters usage. I know I am loathe to use it even when it would be useful for this simple reason. And that has also has a lot to do with the fact that the last word of the method's name, "object", is complete redundant. Of course it is a object! Ruby is an OOPL! So just shortening it to #eachwith alone would make a big difference.

Beyond that, one might argue the block parameters should be in the opposite order to align with #inject, but that isn't at all important. More significant would be to default the argument to an empty hash, since that would be the most common case. I think that's a good idea so the common case can be more concise. But in doing that, it makes more sense to call the method #map_with and have it return the object.

So the doubles example would be:

doubles = numbers.map_with{ |n, h| h[n] = n * 2 }

And one could also do things like:

doubles = numbers.map_with([]){ |n, a| a << n * 2 }

It's a very versatile method and the name makes sense.
=end

#4 Updated by Rodrigo Rosenfeld Rosas over 1 year ago

Hi Thomas,

I agree that object is a redundant suffix and that mapwith is probably a better description than eachwith or eachwith_object.

But it makes sense to me that eachwithobject should yield(item, object) because of the name of the method. I do agree that the order of the arguments isn't very important but I would prefer to have the returning value to become first as it seems more natural to me and it also aligns well with inject like you said.

Now, what I'm not comfortable with is using map_with with {} as the default argument. I agree this is the commonest case, this is not the issue. The problem is that this doesn't read well for someone trying to understand the code:

doubles = numbers.map_with{ |n, h| h[n] = n * 2 }

This reads much better:

doubles = numbers.map_with({}){ |n, h| h[n] = n * 2 } # although I'd reorder the arguments of the closure.

So, if it was my decision to design the language I would probably deprecate eachwithobject in favor of map_with, I'd replace the order of the arguments in the block and would also create another method:

def hashmap(&block)
map
with({}, &block)
end

Instead of hashmap I'd also support the idea of using toh as suggested by someone a while back:

doubles = numbers.to_h{|h, n| h[n] = n * 2 }

#5 Updated by Jeremy Kemper over 1 year ago

The common thread here is that people want a hash conversion in an enumerable chain, where it feels fluent and natural, rather than wrapping the result with Hash[] which makes the code read backward. #eachwithobject is wonderful, but verbose for this use.

Ruby has the idea of an association already: a key and value paired together. It's used by Array#assoc to look up a value from a list of pairs and by Hash#assoc to return a key/value pair. Building up a mapping of key/value pairs is associating keys with values. So consider Enumerable#associate which builds a mapping by associating keys with values:

module Enumerable
# Associates keys with values and returns a Hash.
#
# If you have an enumerable of keys and want to associate them with values,
# pass a block that returns a value for the key:
#
# [1, 2, 3].associate { |i| i ** 2 }
# # => { 1 => 1, 2 => 4, 3 => 9 }
#
# %w( tender love ).associate &:capitalize
# # => {"tender"=>"Tender", "love"=>"Love"}
#
# If you have an enumerable key/value pairs and want to associate them,
# omit the block and you'll get a hash in return:
#
# [[1, 2], [3, 4]].associate
# # => { 1 => 2, 3 => 4 }
def associate(mapping = {})
if blockgiven?
each
withobject(mapping) do |key, object|
object[key] = yield(key)
end
else
each
with_object(mapping) do |(key, value), object|
object[key] = value
end
end
end
end

#6 Updated by John Firebaugh over 1 year ago

I reviewed my own use of Hash[], and in the majority of cases, I'm transforming both the key and value. So I would prefer a more flexible definition:

module Enumerable
# Associates keys with values and returns a Hash.
#
# If you have an enumerable of keys and want to associate them with values,
# pass a block that returns a value for the key:
#
# %w( tender love ).associate &:capitalize
# # => {"tender"=>"Tender", "love"=>"Love"}
#
# [1, 2, 3].associate { |i| [i * 2, i ** 2] }
# # => { 2 => 1, 4 => 4, 6 => 9 }
#
# If you have an enumerable key/value pairs and want to associate them,
# omit the block and you'll get a hash in return:
#
# [[1, 2], [3, 4]].associate
# # => { 1 => 2, 3 => 4 }
def associate(mapping = {})
if blockgiven?
each
withobject(mapping) do |key, object|
key, value = yield(key)
object[key] = value
end
else
each
with_object(mapping) do |(key, value), object|
object[key] = value
end
end
end
end

#7 Updated by John Firebaugh over 1 year ago

john_firebaugh (John Firebaugh) wrote:

I reviewed my own use of Hash[], and in the majority of cases, I'm transforming both the key and value. So I would prefer a more flexible definition:

module Enumerable
# Associates keys with values and returns a Hash.
#
# If you have an enumerable of keys and want to associate them with values,
# pass a block that returns a value for the key:
#
# %w( tender love ).associate { |n| [n, n.capitalize] }
# # => {"tender"=>"Tender", "love"=>"Love"}
#
# [1, 2, 3].associate { |i| [i * 2, i ** 2] }
# # => { 2 => 1, 4 => 4, 6 => 9 }
#
# If you have an enumerable key/value pairs and want to associate them,
# omit the block and you'll get a hash in return:
#
# [[1, 2], [3, 4]].associate
# # => { 1 => 2, 3 => 4 }
def associate(mapping = {})
if blockgiven?
each
withobject(mapping) do |key, object|
key, value = yield(key)
object[key] = value
end
else
each
with_object(mapping) do |(key, value), object|
object[key] = value
end
end
end
end

#8 Updated by John Firebaugh over 1 year ago

john_firebaugh (John Firebaugh) wrote:

john_firebaugh (John Firebaugh) wrote:

I reviewed my own use of Hash[], and in the majority of cases, I'm transforming both the key and value. So I would prefer a more flexible definition:

module Enumerable
# Associates keys with values and returns a Hash.
#
# If you have an enumerable of keys and want to associate them with values,
# pass a block that returns a tuple for the key and value:
#
# %w( tender love ).associate { |n| [n, n.capitalize] }
# # => {"tender"=>"Tender", "love"=>"Love"}
#
# [1, 2, 3].associate { |i| [i * 2, i ** 2] }
# # => { 2 => 1, 4 => 4, 6 => 9 }
#
# If you have an enumerable key/value pairs and want to associate them,
# omit the block and you'll get a hash in return:
#
# [[1, 2], [3, 4]].associate
# # => { 1 => 2, 3 => 4 }
def associate(mapping = {})
if blockgiven?
each
withobject(mapping) do |key, object|
key, value = yield(key)
object[key] = value
end
else
each
with_object(mapping) do |(key, value), object|
object[key] = value
end
end
end
end

#9 Updated by John Firebaugh over 1 year ago

Ugh, sorry for the spam. I didn't realize "editing" a comment actually posts a new one.

#10 Updated by Jeremy Kemper over 1 year ago

John, I have a bit of usage like that. Consider that collection.map { |element| [key, value] }.associate handles that cleanly.

In my code survey, nearly all my uses of Hash[] and #inject are covered by #associate - associating an collection of keys with calculated values.

For more complex scenarios, using more verbose, powerful API like #inject, #eachwithobject, or #map + #associate feels appropriate.

#11 Updated by Marc-Andre Lafortune over 1 year ago

Hi,

bitsweat (Jeremy Kemper) wrote:

The common thread here is that people want a hash conversion in an enumerable chain, where it feels fluent and natural, rather than wrapping the result with Hash[] which makes the code read backward. #eachwithobject is wonderful, but verbose for this use.

Ruby has the idea of an association already: a key and value paired together. It's used by Array#assoc to look up a value from a list of pairs and by Hash#assoc to return a key/value pair. Building up a mapping of key/value pairs is associating keys with values. So consider Enumerable#associate which builds a mapping by associating keys with values:

Exactly.

Here's the secret plan I propose:
1) Let's convince Matz about Enumerable#to_h by adding your +1 to #7292.
2) When that's done, and only when that's done, we can convince Matz that 'associate' is the right name for a nifty method that associates keys with values. See my 1 min slide in #4151 . I believe my proposal agrees with yours and it also handles collisions and the special case of Hash#associate.
Note that we should start with Enumerable#to_h. If we have it, then we can see it's probably best if [].associate (with no block) returns an enumerator, not a hash.
3) After that, we can see if we can come up with a general 'categorize' that could replace map{ [k, v] }.to_h as well as more complex nested cases.

I'd also propose you move the discussion of these features from here (a rejected request about an alias of mapwithobject!) to the right issues.

#12 Updated by Nathan Broadbent over 1 year ago

So consider Enumerable#associate which builds a mapping by associating
keys with values

I like Enumerable#associate. It would cover many of the cases where
eachwithobject({}) is currently used.

However, I think '[[1,2],[3,4]].to_h' would be better for when an Array is
already in the 'associated' form. See #7292 for that discussion:
https://bugs.ruby-lang.org/issues/7292https://bugs.ruby-lang.org/issues/7292#change-32586
.

If the #toh method was also available, then #associate could require a
block, and it would be the inverse of Active Support's 'index
by' method
(it would yields values instead of keys):
http://api.rubyonrails.org/classes/Enumerable.html#method-i-index_by

I'm still wondering if there should be a method that allows you to return
both key and value, but 'map {|e| [e * 2, e ** 2] }.to_h' would probably
be sufficient.

(sorry for bringing #7292 into the discussion, but I feel like it's a
related issue.)

#13 Updated by Jeremy Kemper over 1 year ago

Some background:

#4151 proposes an Enumerable#categorize API, but it's complex and hard to understand its behavior at a glance.
#7292 proposes an Enumerable#toh == Hash[...] API, but I don't think of association/pairing as explicit coercion, so #toh feels misfit.

Associate is a simple verb with unsurprising results. It doesn't introduce ambiguous "map" naming. You associate an enumerable of keys with yielded values.

Some before/after examples:

Before: Hash[ filenames.map { |filename| [ filename, downloadurl(filename) ]}]
After: filenames.associate { |filename| download
url filename }

=> {"foo.jpg"=>"http://...", ...}

Before: alphabet.eachwithindex.eachwithobject({}) { |(letter, index), hash| hash[letter] = index }
After: alphabet.eachwithindex.associate

=> {"a"=>0, "b"=>1, "c"=>2, "d"=>3, "e"=>4, "f"=>5, ...}

Before: keys.eachwithobject({}) { |k, hash| hash[k] = self[k] } # a simple Hash#slice
After: keys.associate { |key| self[key] }

Apologies for hijacking this rejected ticket with a different proposal. I will open a new feature request if anyone is positive on this usage.

#14 Updated by Nathan Broadbent over 1 year ago

Associate is a simple verb with unsurprising results. It doesn't
introduce ambiguous "map" naming. You associate an enumerable of keys with
yielded values.
...
Apologies for hijacking this rejected ticket with a different proposal. I
will open a new feature request if anyone is positive on this usage.

Yep, I'll add my +1 to this feature request. I think #associate is a great
name and would be a very useful method.

#15 Updated by Rodrigo Rosenfeld Rosas over 1 year ago

I like the "associate" name but I prefer the implementation to be like the hash_map described above. I have lots of situations where the element of the Enumerable is not the key of the resulting hash. Your suggestions sounds inefficient to me for this case:

collection.map { |v| [v.attributeid, v.somevalue] }.associate

I guess this will always perform worse than:

collection.associate{|h, v| h[v.attributeid] = v.somevalue }

#16 Updated by Yukihiro Matsumoto over 1 year ago

In the discussion, you guys came up with several new ideas, and they are welcome.
Submit those ideas as issues.

  • eachwith alias to eachwith_object
  • Enumerable#associate

Matz.

#17 Updated by Jeremy Kemper over 1 year ago

Thank you. Enumerable#associate proposed: #7341

Also available in: Atom PDF