Skip to content
← All posts
6 min read Dawid Skłodowski

Ruby 2.1: refinements, required keyword arguments, and a smarter GC

Ruby 2.1.0 shipped on Christmas Day, as tradition demands. The headline is a generational garbage collector, but the language changes — required keyword args, refinements, Exception#cause — are what will change how we write code.

Ruby 2.1.0 landed on Christmas Day, keeping the project’s lovely habit of shipping a release under the tree. It is a measured upgrade rather than a revolution — but that framing undersells it. Two language features change daily code, one runtime change quietly makes most Rails apps faster for nothing, and a clutch of smaller additions remove papercuts you have lived with for years. Here is what we will actually use, and why.

A generational garbage collector (RGenGC)

The headline for production is the new generational GC. The insight it exploits is old and thoroughly proven: most objects die young. A web request allocates thousands of short-lived objects — strings, hashes, intermediate arrays — that become garbage almost immediately, while a much smaller set (your loaded classes, long-lived caches, the connection pool) survives for the life of the process.

Pre-2.1, Ruby’s mark-and-sweep collector walked every live object on every collection, paying again and again to re-examine long-lived objects that were never going to be freed. RGenGC splits the heap into young and old generations and runs cheap minor GCs that scan only the young objects, promoting survivors to the old generation and then mostly leaving them alone. Expensive major GCs that scan everything still happen, just far less often.

The genuinely impressive engineering is that this is backward compatible at the C-extension level. A generational collector normally needs a “write barrier” — notification whenever an old object is made to point at a young one — and existing C extensions know nothing about barriers. The team’s solution treats unbarriered objects as “shady” and handles them conservatively, so extensions that do not opt in stay correct (they just miss some of the speed-up). The result: you upgrade, and a typical Rails request — which is an allocation firehose of young, doomed objects — sees a real latency improvement with zero code changes.

If you want to see it working, GC.stat exposes the counters (minor vs major GC counts, heap slots, old-object counts), and the RUBY_GC_HEAP_* environment variables let you tune initial heap size and growth for a long-running server. Most teams will not need to touch those — the default behaviour is the win.

Required keyword arguments

Ruby 2.0 introduced keyword arguments, but every keyword needed a default value — there was no way to declare one mandatory. 2.1 closes the gap: a keyword with no default is now required, and omitting it raises ArgumentError.

# Ruby 2.0 — the default was a lie; nil was not actually a valid amount
def transfer(from:, to:, amount: nil)
  raise ArgumentError, "amount required" if amount.nil?
  # ...
end

# Ruby 2.1 — say exactly what you mean
def transfer(from:, to:, amount:)
  # all three are mandatory AND named at the call site
end

transfer(from: alice, to: bob, amount: 50)
transfer(from: alice, to: bob)   # => ArgumentError: missing keyword: amount

This is a bigger deal than it first looks. Positional arguments are a readability tax at the call site: charge(account, 50, true, false) tells you nothing about what true and false mean, and you cannot reorder or skip them. The old workaround — an options hash with manual fetch/validation — was boilerplate that every codebase reinvented. Required keywords give you self-documenting calls and enforced presence and free ordering, with no validation code at all. For any method with more than two parameters, or any boolean flag, this is the new default style, and we are already reaching for it everywhere.

Refinements: monkey-patching with a blast radius

Refinements graduate from “experimental” with a clearer, safer design. The problem they solve is the dark side of Ruby’s openness — monkey-patching. Reopen String to add a method and you have changed String for the entire process: your code, every gem, the framework, forever. That is how two libraries end up each defining String#truncate differently and silently breaking one another, with whichever loaded last winning.

A refinement is a monkey-patch with a switch. You define the change inside a module, where it lies dormant until a file (or, new in 2.1, a module) explicitly activates it with using — and crucially, the activation is lexically scoped:

module Squishing
  refine String do
    def squish
      strip.gsub(/\s+/, " ")
    end
  end
end

# only code that opts in sees #squish:
using Squishing
"  hello   world  ".squish   # => "hello world"

Without the using, String#squish does not exist. No other file, gem, or framework component is affected — the change is contained to the lexical scope that asked for it. This is the long-promised answer to “I want to add a method to a core class without poisoning the global namespace”. The ergonomics in 2.1 still have sharp edges — the lexical scoping rules are subtle, you cannot refine inside a block, and method lookup with refinements active has surprised people — so it is not a wholesale replacement for Module#prepend or composition. But the direction is exactly right: the convenience of monkey-patching with a contained blast radius, and it is the tool to reach for when you would otherwise reopen a core class.

Exception#cause: the chain is recorded for you

A quiet favourite. When you rescue one error and raise another, Ruby 2.1 now automatically records the original as the new exception’s cause — no manual wrapping required:

begin
  parse_config
rescue SyntaxError
  raise ConfigError, "config file is invalid"   # original SyntaxError is preserved
end

ConfigError#cause returns the underlying SyntaxError, and a well-behaved backtrace printer walks the chain. No more swallowing the real failure when you re-raise a friendlier one — the trail back to the root cause is kept for you.

The smaller niceties

A handful of changes round it out, each removing a small daily annoyance:

  • def returns the method name as a symbol. This makes method decorators clean: private def helper; end just works, no more private :helper on a separate line after the definition.
  • Array#to_h and Enumerable#to_h. Building a hash from pairs no longer needs Hash[...] gymnastics: [[:a, 1], [:b, 2]].to_h, or items.map { |i| [i.id, i] }.to_h.
  • String#scrub replaces invalid UTF-8 bytes with a replacement character — the clean fix for the invalid byte sequence errors that plague apps handling messy external text.
  • String-literal freezing is optimised. "text".freeze is now special-cased to return a cached frozen instance instead of allocating, which foreshadows the frozen-string-literal world to come and is worth using for hash keys and constants in hot paths today.
  • Rational and Complex literals gain suffixes: 42r is a Rational, 2i a Complex — handy for anyone doing exact arithmetic.

Should you upgrade?

Yes, and without much ceremony. The GC improvement is the kind of free win you take immediately: upgrade in staging, watch your memory and latency graphs settle, do nothing else. The language features are purely additive, so existing code keeps working — you adopt required keyword arguments as you touch methods, reach for refinements the next time you are tempted to monkey-patch a core class, and get Exception#cause and the to_h/scrub conveniences for free.

The through-line of 2.1 is maturity rather than novelty. Ruby is being tuned for the large, long-running applications people actually run in production: a faster collector for the server, clearer argument contracts for the codebase, a disciplined alternative to its own most dangerous feature, and better error provenance for the 3 a.m. debugging session. None of it is flashy, and all of it is the kind of thing you are quietly grateful for six months later. A good Christmas present.