public
Created

Enumerable#associate

  • Download Gist
gistfile1.md
Markdown

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 the each_with_object block because of the way |(a, b), c|-style args are handled.

def associate(mapping = {})
  each_with_object(mapping) do |(key, value), object|
    object[key] = block_given? ? yield(key) : value
  end
end

Seems to work as expected. But maybe you prefer it the original way. :)


In case it isn't clear:

[1].each_with_object({}) { |(a, b), c| p [a, b, c] }
#=> [1, nil, {}]

@aprescott, I think the idea is to only check block_given? once, even if that requires a more verbose method. Checking it once per iteration would give lower performance. But also note that this will probably be rewritten in C :)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.