From a Clever Hack to a Language-Level Experiment

Sometimes a piece of code that looks odd at first glance turns out to be a gateway to something much larger. In this case, a clever-but-obscure use of Ruby's fetch method led to a refactoring exercise, and eventually to a prototype change in TruffleRuby itself — along the way uncovering a performance issue in the implementation.

The original code was production Ruby that extracted a value from a nested hash by chaining two calls to fetch, each passing two arguments. The second argument in each call was a default value, which made the code functionally correct but hard to read at a glance.

Understanding why the code worked required a quick review of Hash#fetch and its optional default argument. When a key is missing, fetch raises a KeyError — unless you supply a default value or a block, in which case that value (or the block's result) is returned instead. The original code exploited this: if the top-level key was absent, the first fetch returned an empty hash. The chained second fetch then always failed to find its key and returned the provided default — an instance of IdentityObject.

That's clever, but it's also convoluted. Ruby already has Hash#dig, which traverses nested hashes and returns nil when a key is missing. Refactoring the original code with dig made it far more readable in the happy path.

But dig isn't as versatile as fetch. With fetch, you can choose to raise an error on a missing key, return nil, or provide a meaningful default — all within a single call. With dig, you'd have to add explicit logic to distinguish between a missing key and a key explicitly set to nil.

What if Ruby had a method that combined dig's traversal with fetch's flexibility? The simplest approach would be to monkey-patch Hash in a regular Ruby project. But there's a more interesting option: add the method to a Ruby implementation itself. Since most of the standard Ruby implementation (MRI) is written in C, and the learning curve there is steep, a better candidate is TruffleRuby.

Prototyping dig_fetch in TruffleRuby

TruffleRuby is an alternative Ruby implementation built on GraalVM and the Truffle language framework. Its goal is to run idiomatic Ruby code faster, and one of its practical advantages for this experiment is that parts of the language are themselves written in Ruby. That makes it possible to make a language-level change without touching C.

For the prototype, the new method was named dig_fetch. The first version handled the simplest case: fetching a single value from a hash. From there, the method evolved step by step:

  • Raise a KeyError when a key is missing, mirroring fetch behaviour.
  • Handle explicit nil values correctly — a key present with a nil value should return nil, not raise an error. This was achieved using TruffleRuby's Primitive module, which exposes hash_get_or_undefined and undefined? to distinguish between a missing key and one that exists with a nil value.
  • Add recursive traversal, inspired by the existing implementation of dig, so the method could dig through nested hashes.
  • Accept a default value via a block, matching fetch's flexibility.

Writing the method was smooth. But a language-level change should be justified by more than just correctness — it needs to perform well. That meant benchmarking.

Benchmarking: A Steep Drop in Performance

The benchmark compared dig, fetch, and the new dig_fetch using benchmark-ips. Tests measured how many iterations each method completed in five seconds, across hashes with three, six, and nine levels of nesting.

At a depth of three, results looked good: dig_fetch performed similarly to the others, at around 458.69 million iterations.

At a depth of six, a serious problem appeared. Both dig_fetch and dig degraded noticeably, while fetch held up far better. At depth nine, the situation worsened: dig_fetch and dig dropped to about 12.7 million iterations, while fetch still managed roughly 164 million.

The performance cliff wasn't unique to the new method — dig, which inspired the recursive implementation of dig_fetch, suffered the same fate. That pointed to a common cause.

Both methods are implemented recursively. TruffleRuby, as an optimizing implementation, tries to inline recursive calls into a single body of machine code, but there's a limit — infinite inlining would produce infinite code. An iterative solution with a loop starts within a single optimized body from the beginning, avoiding this problem altogether.

The investigation that began with an odd use of fetch had uncovered an opportunity to rework dig itself with an iterative implementation.

Iterative dig Lands in TruffleRuby

With the recursive implementation confirmed as the bottleneck, the next step was to bring the problem to the TruffleRuby team. Chris Seaton, the language implementation’s founder and maintainer, was available to help ship a fix for the performance degradation. The plan: replace the recursive traversal with an iterative loop.

To keep things clean, the iterative logic went into a new package called Diggable. That choice wasn’t arbitrary—dig is also defined on Array and Struct in Ruby. A shared package makes it straightforward to update all three implementations to use the same behavior later. For now, though, the focus stayed on Hash#dig.

Inside Diggable, a dig method was added with a loop that runs once for each key passed in. The refactor preserved existing behavior while eliminating the recursion that caused the slowdown.

Measuring dig Again

The performance results after this change were dramatic. Where the recursive dig could complete roughly 2.5M iterations per second on a hash with nine nested keys, the iterative version pushed that to about 16M iterations per second. The fix was shipped in two pull requests to TruffleRuby: #2300 and #2301.

Line graph of Performance of Hash#dig in TruffleRuby

Applying the Same Fix to dig_fetch

With dig solved, the same approach was applied to the custom dig_fetch method. The implementation in the Diggable package ended up looking very similar to the iterative dig. Once the method was confirmed to work, the benchmarks were run again.

The iterative version of dig_fetch performed just as well as the improved dig. Executing roughly 15.5M times per second on a hash with nine nested keys, it far outpaced the recursive implementation’s ~2.5M. Comparing the two side by side, the difference is stark.

Line graph of Performance of Hash#dig in TruffleRuby

The Original Code, Refactored

With performance in good shape, it was finally time to swap the new method into the production code that started this investigation. The refactor made the original code considerably more readable.

What Didn’t Make the Cut

Although dig_fetch works well, it isn't ready for broad adoption. The work here deliberately ignored interoperability with Array and Struct—both of which define dig—and adding the method to TruffleRuby would also require mirroring the change in MRI. That means convincing the Ruby community to accept a new method, which is a much larger undertaking.

Still, the investigation delivered real, tangible value. While dig_fetch didn't become part of the language, the performance of dig itself improved significantly in TruffleRuby. That alone puts the existing dig method in a much better position for production workloads.