We've seen lots of Ruby feature requests about converting an Enumerable to a Hash: to_h
, map_hash
, map_to
, each_with_hash
, etc. Let's look at one common, simple case of this.
Building a key/value mapping from a collection of keys is awkward. We commonly see Hash[*collection.map { |element| [element, calculate(element)] }]
or collection.each_with_object({}) { |element, hash| hash[element] = calculate(element) }
. Both are verbose. They require boilerplate code that's not relevant to the programmer's intent: to associate an enumerable of keys with calculated values.
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:
# Associate filenames with URLs. Before:
Hash[ filenames.map { |filename| [ filename, download_url(filename) ]}]
# After:
filenames.associate { |filename| download_url filename }
# => {"foo.jpg"=>"http://...", ...}
# Associate letters with their position in the alphabet. Before:
alphabet.each_with_index.each_with_object({}) { |(letter, index), hash| hash[letter] = index }
# After:
alphabet.each_with_index.associate
# => {"a"=>0, "b"=>1, "c"=>2, "d"=>3, "e"=>4, "f"=>5, ...}
# A simple Hash#slice(*keys). Before:
keys.each_with_object({}) { |k, hash| hash[k] = self[k] }
# After:
keys.associate { |key| self[key] }
"Associate" is a simple verb with unsurprising results. You associate an enumerable of keys with yielded 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 block_given?
each_with_object(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
You should be able to shift the
block_given?
inside theeach_with_object
block because of the way|(a, b), c|
-style args are handled.Seems to work as expected. But maybe you prefer it the original way. :)
In case it isn't clear: