Refine Your Ruby Util Objects

For some time now I've followed a pattern of using utilities to avoid extending core Ruby objects. While this approach avoids the perils of monkey patching, the experience has always bothered me; it's not very Ruby-like.

Last week @searls tweeted this gist demonstrating his recent usage of Ruby Refinements. Refinements are not that new, but for some reason the feature never made it into my toolbox.

Until this weekend, that is. While refactoring some old code I realized that refinements would offer a much more Ruby-like experience than my dumb util approach. Here's an example from one of my projects:

module Utils
  module Dup
    UNDUPABLE = [Symbol, Fixnum, NilClass, TrueClass, FalseClass]

    def self.deep_dup(dupable)
      return dupable if UNDUPABLE.include?(value.class)

      if dupable.is_a?(Hash)
        dupable.each_with_object({}) do |(key, value), hash|
          hash[key.deep_dup] = value.deep_dup
        end
      elsif dupable.is_a?(Array)
        dupable.each_with_object([]) do |value, array|
          array << value.deep_dup
        end
      else
        dupable.dup
      end
    end
  end
end

Utils::Dup.deep_dup just returns a deep copy of the object. It works, but that's about all it has going for it. The need to check if the object is a Hash or Array is just awful. And using it is clunky too. I mean really:

Utils::Dup.deep_dup({ "foo" => "bar" })

Refinements solves both of these problems. Consider if you will:

module SuperDuper
  UNDUPABLE = [Symbol, Fixnum, NilClass, TrueClass, FalseClass]

  refine Object do
    def deep_dup
      if UNDUPABLE.include?(self.class)
        self
      else
        dup
      end
    end
  end

  refine Array do
    def deep_dup
      each_with_object([]) do |value, array|
        array << value.deep_dup
      end
    end
  end

  refine Hash do
    def deep_dup
      each_with_object({}) do |(key, value), hash|
        hash[key.deep_dup] = value.deep_dup
      end
    end
  end
end

You'll notice that the method bodies are identical to how we'd write this as a straight up monkey patch, so I call that a win for code readability. Using the refinements version of deep_dup also feels much more Ruby-like:

using SuperDuper
{ "foo" => "bar" }.deep_dup

And there you have it. The benefits of monkey patching, without the monkeys. Now go read more about refinements and put them to use in your projects.

Talk to me about this post: @bryanp