ActiveSupport: Hash Extensions

At RailsConf this year, Aaron Patterson talked a bit about “aggressively trimming negativity” from his life and focusing on the positive things. I’m working on that as well, which is part of the genesis of this Rails source journal. People like to complain about things, the more familiar the more often it sometimes seems. Before I started this project, I’d fill gaps in time with reading Hacker News, but the negativity really wore on me. Now, I instead get to go digging for really cool stuff in Rails that informs my every day work. I love it.

Recursive Hash Merging

For instance, last night I came across this cool bit of recursion in the deep merge extension to Hash:

(from core_ext/hash/deep_merge.rb)

class Hash
  # Returns a new hash with +self+ and +other_hash+ merged recursively.
  #   h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
  #   h2 = { x: { y: [7,8,9] }, z: 'xyz' }
  #   h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"}
  #   h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]}
  def deep_merge!(other_hash, &block)
    other_hash.each_pair do |k,v|
      tv = self[k]
      if tv.is_a?(Hash) && v.is_a?(Hash)
        self[k] = tv.deep_merge(v, &block)
        self[k] = block && tv ?, tv, v) : v

Having recently finished “The Little Schemer”, I loved seeing recursion pop out of my Rails source journey, especially so quickly! The pattern for understanding a good recursive function from TLS is quite simply:

  • always ask two questions: are we done? and else (because else is always the second question)
  • always progress towards termination via changing an argument each recursion (which is in the else)

Ok, now go buy “The Little Schemer”, it’s really that good…did I mention it’s written in the Socratic style?

Back to our regularly scheduled program…“Deep” as a prefix generally means that it’s applied at every level of a nested data structure, so this deep merge method wants to apply at every level. Does it meet TLS guidelines? Of course! Each_pair is guaranteed to terminate when the keys are exhausted so at the outer level we are fine. Inside the pairwise block we see recursion applied when either hash’s value is a Hash. So our done condition is really “am I not a hash?” with an else of “merge the subhashes and reduce my hash key space by one key.”

There’s some extra functionality here with the optional block format (trimmed here for brevity), but that is pretty cool too. Many many things in the source are permissive by nature…want to pass in a block to override the logic? Go right ahead! Again, more Ruby is nice.

Default all the things!

I seem to be drawn to pointing out all the ways you can manage to set defaults inside a method. Here’s another!

(from core_ext/hash/reverse_merge.rb)

class Hash
  # Merges the caller into +other_hash+. For example,
  #   options = options.reverse_merge(size: 25, velocity: 10)
  # is equivalent to
  #   options = { size: 25, velocity: 10 }.merge(options)
  # This is particularly useful for initializing an options hash
  # with default values.
  def reverse_merge!(other_hash)
    # right wins if there is no left
    merge!( other_hash ){|key,left,right| left }

This would be handy in those cases where I have a bunch of defaults that I want to set and I want to make it clear logically which hash is overriding which. It has an alias of reverse_update!, which may be a bit easier to remember.

Roll your own strong params

Since I haven’t made it to any of the actual framework code, it’s very possible that this is used in strong params, but regardless, check it out! Hash#slice gives you a convenient way to limit the keys of a hash to a specified superset. That could be quite handy for…wait for it…OPTIONS HASHES!

(from core_ext/hash/slice.rb)

class Hash
  # Slice a hash to include only the given keys. This is useful for
  # limiting an options hash to valid keys before passing to a method:
  #   def search(criteria = {})
  #     criteria.assert_valid_keys(:mass, :velocity, :time)
  #   end
  #   search(options.slice(:mass, :velocity, :time))
  # If you have an array of keys you want to limit to, you should splat them:
  #   valid_keys = [:mass, :velocity, :time]
  #   search(options.slice(*valid_keys))
  def slice(*keys)! { |key| convert_key(key) } if respond_to?(:convert_key, true)
    keys.each_with_object( { |k, hash| hash[k] = self[k] if has_key?(k) }

Reading the implementation, it’s kind of neat the reversal that happens in that last line. Rather than trimming the existing hash, it pulls a Judo move and builds a new Hash with only the keys that pass the has_key? check. I have to think that is to reduce the number of iterations and make the code a tiny bit more performant.

That’s all for now, my pup wants to play.

Until next time, have fun reading the source!

Published under programming