Ruby 2.3: frozen string literals and the safe-navigation operator
Ruby 2.3.0 lands on Christmas Day with two changes you will type every day — the frozen-string-literal pragma and the &. safe-navigation operator — plus dig, did_you_mean, and more. A practical tour.
Ruby 2.3.0 has arrived on Christmas Day, as it does, and this one is full of small features you will actually type. Where the last couple of releases were mostly about the garbage collector working harder behind the scenes, 2.3 changes the code you write — a new operator, a new file-level pragma, a couple of genuinely handy methods, and a friendlier experience when things go wrong. Here is the practical tour.
The safe-navigation operator: &.
This is the headline, and it is borrowed from C# and Groovy where it has long earned
its keep. The “lonely operator” &. calls a method only if the receiver is not nil,
returning nil instead of blowing up:
# before — the defensive dance
name = user && user.profile && user.profile.name
# Ruby 2.3
name = user&.profile&.name
If user is nil, the whole expression short-circuits to nil instead of raising
NoMethodError: undefined method 'profile' for nil. This kills one of the most
common, ugly patterns in Ruby — the chain of && guards, or the try(:method) calls
Rails developers reach for. It reads cleanly and says exactly what it means: “call this
if there’s something to call it on.”
A word of caution, because it is easy to overuse: &. is a tool for the cases where
nil is a legitimate, expected value (an optional association, a config that might
be absent). It is not a way to paper over nils that should never occur — if a value
being nil indicates a bug, you want the NoMethodError to surface it, not a &. to
swallow it into a silent nil that fails confusingly three layers away. Use it to
handle expected absence, not to hide unexpected absence.
Frozen string literals: a pragma and a future
The second big change is the frozen_string_literal pragma — a magic comment at the
top of a file that makes every string literal in it frozen (immutable):
# frozen_string_literal: true
GREETING = "hello" # this string is frozen — cannot be mutated
GREETING << " world" # => RuntimeError: can't modify frozen String
There are two reasons this matters. The first is performance: in Ruby, every string literal normally allocates a brand-new object each time it is evaluated, so a literal in a hot loop creates garbage on every pass. A frozen literal can be a single cached, shared, immutable object — fewer allocations, less GC pressure. For literals used as hash keys and constants in hot paths, that is a real saving. The second is correctness: immutable strings cannot be accidentally mutated by some far-off code holding a reference, which removes a category of spooky-action-at-a-distance bugs.
In 2.3 this is opt-in per file via the pragma, and it is clearly the direction of
travel — the long-term plan is for frozen string literals to become the default in a
future Ruby. Adopting the pragma now future-proofs your files and earns the
performance benefit today. The one adjustment: where you genuinely need a mutable
string, allocate one explicitly with String.new or +"..." (the unary plus returns
a mutable copy).
dig: reaching into nested structures safely
Hash#dig and Array#dig are small but you will use them constantly. They walk a
nested structure by a sequence of keys, returning nil the moment any level is
missing instead of raising:
data = { user: { address: { city: "Kraków" } } }
data.dig(:user, :address, :city) # => "Kraków"
data.dig(:user, :company, :name) # => nil (no raise on the missing :company)
# vs the old, fragile:
data[:user][:company][:name] # => NoMethodError on nil[:name]
This is the safe-navigation idea applied to data structures, and it is a godsend for
the deeply-nested hashes you get from parsing JSON API responses, where any level
might be absent. One call replaces a pile of && checks or fetch chains.
did_you_mean: typos get a hint
A quality-of-life win for everyone: the did_you_mean gem is now bundled and on by
default, so a misspelled method or variable name comes back with a suggestion instead
of a bare error:
NoMethodError: undefined method `lenght' for "hello":String
Did you mean? length
It is a small thing that saves real time, especially for newcomers, and it costs you nothing.
The smaller additions
A few more worth knowing:
Enumerable#grep_vis the inverse ofgrep— select everything that does not match:lines.grep_v(/^#/)drops comment lines.Comparable#clampconstrains a value to a range:score.clamp(0, 100)instead of nestedmin/maxcalls.Hash#to_proclets a hash act as a lookup block:ids.map(&lookup_hash)maps each id through the hash.- The “frozen string” deprecation tooling (
--enable-frozen-string-literaland a debug mode) helps you find code that would break under future frozen-by-default behaviour.
Should you upgrade?
Yes — and unusually, this is a release whose value shows up in your editor, not just
your metrics. The safe-navigation operator and dig will clean up nil-handling code
you write every day; the frozen_string_literal pragma earns a performance benefit and
prepares you for the future default; and did_you_mean quietly makes every error
message more helpful. None of it is risky to adopt — &., dig, and the niceties are
purely additive, and the frozen-string pragma is opt-in per file so you can roll it out
gradually.
The pattern of recent Ruby releases continues: the runtime keeps getting leaner and the
language keeps getting small, sharp ergonomic improvements that make everyday code
cleaner. 2.3 is a particularly nice batch of them — the kind of release where, a month
after upgrading, user&.profile&.name and data.dig(...) already feel like they were
always there.