In Progress
Unit 1, Lesson 21
In Progress

Dynamic Understanding of Dynamic Systems

When I program in Ruby, I do it with a browser tab or three open on the Ruby core documentation. I also get a lot of benefit from modern code editors that can suggest method completions for me.

But in today’s episode, guest chef Paul Stefan Ort reminds us that in a dynamic language these kinds of static documentation can never be authoritative. Ruby’s core classes can be modified at any time, by any code you load. Fortunately, Ruby’s dynamic nature works for us as well, and today Paul will show you how to get comfortable using Ruby to discover exactly what changes a RubyGem makes to system classes. Enjoy!

Video transcript & code

Dynamic Understanding of Dynamic Systems

The ultimate source of truth about the behavior of a dynamic system is the system itself.

The dynamic nature of Ruby makes it possible to introduce changes at any point, enabling powerful features and patterns.

It also means that reading through application code does not necessarily indicate comprehensive system behavior.

This can have subtle implications, which we will explore today.

Let’s consider an example in which troubleshooting an issue requires using Ruby’s dynamic capabilities: - We are serving a JSON API and - We use the standard JSON gem bundled with the core Ruby distribution.


  - JSON endpoints
  - standard `json` gem

Suppose that we are making a rhyming dictionary and need to return a short list of the most popular rhyming words.

We build a Hash containing entries for the most popular words.


  require "json"
  top_words = {}
  Dictionary.top_words.each do |word|
    top_words[word] = Dictionary.top_rhymes(word)
  end
  # top_values = {
  #  "cat" => ["hat", "flat", "bat"],
  #  "bear" => ["where", "care", "there"],
  #  "ant" => ["plant", "chant", "slant"]
  # }

The JSON generated by the standard json gem bundled with Ruby presents keys in the order in which they were defined in the source Hash.


  require "json"
  top_words = {}
  Dictionary.top_words.each do |word|
    top_words[word] = Dictionary.top_rhymes(word)
  end
  # top_values = {
  #  "cat" => ["hat", "flat", "bat"],
  #  "bear" => ["where", "care", "there"],
  #  "ant" => ["plant", "chant", "slant"]
  # }
  top_words.to_json
  # => {"cat":["hat","flat","bat"],"bear":["where","care","there"],"ant":["plant","chant","slant"]}

The rhyming dictionary contains many words, and we want to enable API clients to read up until the word they are interested in and then stop.

Unfortunately, the lists of rhyming words are sorted by popularity, and this new use case requires alphabetical sorting.


  all_words = {}
  Dictionary.words.each do |word|
    all_words[word] = Dictionary.rhyming_words(word)
  end
  all_words.to_json
  # => {"cat":["hat","flat","bat"],"bear":["where","care","there"],"ant":["plant","chant","slant"],"mat":["format","diplomat","combat"],"gnat":["that","chat","habitat"],...}

Suppose that we think about the problem, and find a gem that handles the sorting, json_sorted.  [su_note note_color="#eeeeee" radius="0"]Note: Do not use this gem. It was created strictly as an example for this episode.[/su_note]

Now we load the json_sorted gem where we need to return sorted results.


  all_words = {}
  Dictionary.words.each do |word|
    all_words[word] = Dictionary.rhyming_words(word)
  end
  require "json_sorted"

This ensures that the JSON values returned are properly sorted, satisfying our requirements.


  all_words = {}
  Dictionary.words.each do |word|
    all_words[word] = Dictionary.rhyming_words(word)
  end
  require "json_sorted"
  all_words.to_json
  # => {"ant":["plant","chant","slant"],"bear":["where","care","there"],"cat":["hat","flat","bat"],"gnat":["that","chat","habitat"],"mat":["format","diplomat","combat"],...}

This seems to be a successful outcome; we have the sorting we want, and all we had to do was add a dependency. All we had to do was add a dependency. That sounds a bit suspicious, doesn’t it? We didn’t have to change any code to produce the sorted results. How does that work?

Suppose that some time later we learn that some of the caches in the system are no longer functioning as expected.

We learn that the problem is caused by our change, which inadvertently caused every single JSON-ified Hash in the system to change. This unintended consequence causes confusion elsewhere, because dynamic behavior introduced for one use case had broad impacts.

How can we gain more information about the changes made by gems? Often there is little or no documentation, and even when there is documentation, how can we verify its claims? Fortunately, Ruby’s dynamic programming capabilities are well-equipped to solve problems like this.

Let’s start at the beginning: we know there is a problem with turning Hash values into JSON, so let’s see where Hash#to_json was introduced in the first place.

Starting from scratch, let’s ask whether Hash has a to_json instance method. We see that it does not.


  Hash.instance_methods.include?(:to_json)

Now let’s require the “json” gem and ask again. There it is!


  Hash.instance_methods.include?(:to_json) # => false
  require "json"
  Hash.instance_methods.include?(:to_json) # => true

We can use the instance_methods method to identify the instance methods available on standard objects and classes.

Now we know that the json gem added Hash#to_json. Let’s examine the impact of json_sorted.

Let’s save the version of Hash#to_json provided by the json gem, so that we can compare it to the version from json_sorted

We’ll use the instance_method method, which returns an UnboundMethod object representing the implementation.


  require "json"
  original_implementation = Hash.instance_method(:to_json)
  # => #

Now let’s load the json_sorted gem and capture the implementation of Hash#to_json again.


  require "json"
  original_implementation = Hash.instance_method(:to_json)
  # => #
  require "json_sorted"
  updated_implementation = Hash.instance_method(:to_json)
  # => #

When we compare the two instance method objects, we can see that they are not the same. Now we see the impact of using json_sorted, and can use this information to consider whether a different approach would be more suitable.


  require "json"
  original_implementation = Hash.instance_method(:to_json)
  # => #
  require "json_sorted"
  updated_implementation = Hash.instance_method(:to_json)
  # => #
  original_implementation == updated_implementation
  # => false

What else might have changed? We can build on this technique to identify all of the changes to instance methods of Hash.

After loading the "json" gem, let’s start with a hash in which to store the original instance methods.


  require "json"

  original_implementations = {}

We will go through the instance methods on Hash.


  require "json"

  original_implementations = {}
  Hash.instance_methods do |instance_method_name|
  end

We can use use the instance_method method to capture an object representing the method’s implementation. The resulting object is an UnboundMethod.


  require "json"

  original_implementations = {}
  Hash.instance_methods do |instance_method_name|
    instance_method = Hash.instance_method(instance_method_name)
    # => #
  end

Now let’s save a copy of that method object so that we can compare against it later.


  require "json"

  original_implementations = {}
  Hash.instance_methods do |instance_method_name|
    instance_method = Hash.instance_method(instance_method_name)
    original_implementations[instance_method_name] = instance_method
  end

Having captured a representation of the method implementations, we can now load the json_sorted gem. This sets the stage for identifying the modifications it makes.


  require "json"

  original_implementations = {}
  Hash.instance_methods.each do |instance_method_name|
    instance_method = Hash.instance_method(instance_method_name)
    original_implementations[instance_method_name] = instance_method
  end

  require "json_sorted"

  updated_implementations = {}
  Hash.instance_methods.each do |instance_method_name|
    instance_method = Hash.instance_method(instance_method_name)
  end

Let’s go through the same steps as before, but this time we’ll check for changes against the previously saved implementations. As we identify implementations that differ from the original ones, we’ll add them to the updated_implementations hash, so that we can get a full picture of changes made to the system.


  require "json"

  original_implementations = {}
  Hash.instance_methods.each do |instance_method_name|
    instance_method = Hash.instance_method(instance_method_name)
    original_implementations[instance_method_name] = instance_method
  end

  require "json_sorted"

  updated_implementations = {}
  Hash.instance_methods.each do |instance_method_name|
    instance_method = Hash.instance_method(instance_method_name)
    unless instance_method == original_implementations[instance_method_name]
      updated_implementations[instance_method_name] = instance_method
    end
  end
  puts updated_implementations
  # => {:to_json=>#}

We can expand this approach beyond Hash. Let’s try it with several commonly extended core classes: Array, String, and Time.

Instead of hard-coding the instance_methods lookup to Hash, we will iterate through an array of classes.

We also need to add a level to the original_implementations hash, so that we can store implementations across multiple classes.


  require "json"

  original_implementations = {}
  [Hash, Array, String, Time].each do |klass|
    implementations_for_klass = {}
    klass.instance_methods.each do |instance_method_name|
      implementation = klass.instance_method(instance_method_name)
      implementations_for_klass[instance_method_name] = implementation
    end
    original_implementations[klass] = implementations_for_klass
  end

Now we can load json_sorted and perform the perform the method comparison as previously, this time across multiple classes.

Sure enough, this identifies changed JSON-related behavior in all of those classes!


  require "json"

  original_implementations = {}
  [Hash, Array, String, Time].each do |klass|
    implementations_for_klass = {}
    klass.instance_methods.each do |instance_method_name|
      implementation = klass.instance_method(instance_method_name)
      implementations_for_klass[instance_method_name] = implementation
    end
    original_implementations[klass] = implementations_for_klass
  end

  require "json_sorted"

  updated_implementations = {}
  [Hash, Array, String, Time].each do |klass|
    implementations_for_klass = {}
    klass.instance_methods.each do |instance_method_name|
      implementation = klass.instance_method(instance_method_name)
      original_implementation = original_implementations[klass][instance_method_name]
      unless implementation == original_implementation
        implementations_for_klass[instance_method_name] = implementation
      end
    end
    updated_implementations[klass] = implementations_for_klass
  end
  updated_implementations
  # => {Hash=>{:to_json=>#},Array=>{:to_json=>#},String=>{:to_json=>#},Time=>{:to_json=>#}}

We could trivially extend this to all objects in the system by getting a dynamic list of classes from ObjectSpace.


  ObjectSpace.each_object(Class).to_a
  # => #

We have seen how to use the dynamism of Ruby to build an understanding of changes made to the behavior of a system.

Sometimes we need to assess the impact of a third-party dependency. Sometimes we need to navigate obscure areas of a larger system. Sometimes we may need to audit specific security concerns.

Whatever the case, these reflection techniques provide information that would be difficult if not impossible to gain from simply reading application source code.

Remember, the ultimate source of truth about the behavior of a dynamic system is the system itself.

Happy hacking!


  - use the `instance_methods` method to discover available behaviors
  - use the `instance_method` method to identify changes in system behavior
  - use reflection to demystify “magic” in dependencies

  The ultimate source of truth about the behavior of a dynamic system is the system itself.

Responses