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:
defreturns the method name as a symbol. This makes method decorators clean:private def helper; endjust works, no moreprivate :helperon a separate line after the definition.Array#to_handEnumerable#to_h. Building a hash from pairs no longer needsHash[...]gymnastics:[[:a, 1], [:b, 2]].to_h, oritems.map { |i| [i.id, i] }.to_h.String#scrubreplaces invalid UTF-8 bytes with a replacement character — the clean fix for theinvalid byte sequenceerrors that plague apps handling messy external text.- String-literal freezing is optimised.
"text".freezeis 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:
42ris a Rational,2ia 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.