&. skips the call when the receiver is nil¶
Level: 101 · a first-week reader who writes x.y if x else None in Python
One line: receiver&.method(args) returns nil without calling anything — not even evaluating the arguments — when the receiver is nil, and only nil; dig does the same across nested Hashes and Arrays, and fetch is the strict opposite of the lenient [].
Calling a method on nil is the most common NoMethodError in Ruby, and &. (the "safe navigation" or "lonely" operator, in the language since Ruby 2.3 (Not machine-checked here.)) is the one-character answer: if the receiver is nil, the whole call is skipped and the expression is nil; if not, it is an ordinary call. Two details matter. The arguments are not evaluated when the call is skipped, so a side effect inside them does not happen. And only nil is skipped — false is an object with methods and is called normally. For nested data the same idea is Hash#dig and Array#dig, which stop at the first missing key and return nil instead of raising. The reverse tool is fetch: where h[:missing] quietly gives nil, h.fetch(:missing) raises KeyError, and fetch with a default or a block is the explicit way to say what a missing key means.
Verified output of safe_navigation_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. a method on nil: name.length -> NoMethodError: undefined method 'length' for nil
2. safe navigation: name&.length -> nil, "abc"&.length -> 3
3. the arguments are skipped: nil&.concat(noisy) -> nil
...and evaluated otherwise: "x"&.concat(noisy) -> (argument evaluated) "x!"
4. only nil is skipped: false&.to_s -> "false" (false is a real object with methods)
5. a chain of objects: u1.address&.city -> "Oslo", u2.address&.city -> nil
u2.address.city -> NoMethodError: undefined method 'city' for nil
6. dig through nested Hashes: h.dig(:a, :b, :c) -> 1, h.dig(:a, :x, :c) -> nil
h[:a][:x][:c] -> NoMethodError: undefined method '[]' for nil
7. [] is lenient, fetch is strict: h[:zz] -> nil, h.fetch(:zz) -> KeyError: key not found: :zz
h.fetch(:zz, "default") -> "default"
8. Array#dig and a dead end: [[1, [2]]].dig(0, 1, 0) -> 2, [].dig(0, 1) -> nil
h.dig(:a, :b, :c, :d) -> TypeError: Integer does not have #dig method
9. &. guards one call only: nil&.length.zero? -> NoMethodError: undefined method 'zero?' for nil
nil&.length&.zero? -> nil
Reading the output¶
Row 1 is the error &. exists for: undefined method 'length' for nil. Row 2 is the operator on a nil and on a String. Row 3 shows the argument being skipped: nil&.concat(noisy) prints nothing, while "x"&.concat(noisy) prints "(argument evaluated)" before the result. Row 4: false&.to_s is "false", because only nil short-circuits. Row 5 is the everyday shape — a chain of objects where one link may be nil — with the unguarded version's error beside it.
Rows 6 to 8 are the Hash and Array tools. dig walks :a, :b, :c and returns nil at the first missing key, where h[:a][:x][:c] fails on nil[]; [] on a missing key is nil, fetch raises KeyError: key not found: :zz, and fetch with a default returns it. Array#dig works the same way, and dig into something that is not a container (the 1 at the end of the chain) is a TypeError, not a silent nil. Row 9 is the detail a JavaScript reader needs: &. guards one call. In nil&.length.zero? the &. returns nil and then .zero? is called on that nil normally — so each link that may be nil needs its own &..
Compared with Python¶
Python has no safe-navigation operator; PEP 505 proposed ?. and ?? and was deferred (Not machine-checked here.). The twin uses what is there. The conditional expression (row 2) is the direct translation and, like &., it skips the arguments (row 3); getattr(x, "y", None) does not — it evaluates the call's arguments first, as the printed "(argument evaluated)" shows. Row 4 is the trap in the shorter x and x.upper() idiom: and drops every falsy value, so an empty string never gets its method called, where Ruby's &. and the is not None test skip only None. Rows 6 and 7 are the mirror image of Ruby's defaults: d[k] is strict and raises KeyError, d.get(k) is lenient and returns None, so a chain of .get(k, {}) is Python's dig. There is no dig itself; row 8 writes it in five lines. Row 9 has nothing to measure: each step in a chain needs its own guard.
Verified output of safe_navigation_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. a method on None: name.upper() -> AttributeError: 'NoneType' object has no attribute 'upper'
2. the conditional expression: name.upper() if name is not None else None -> None ; 'abc' -> 'ABC'
3. the arguments are skipped: x.replace('a', noisy()) if x is not None else None -> None
...but not by getattr: getattr(x, 'replace', lambda *a: None)('a', noisy()) -> (argument evaluated) None
4. only None is skipped: '' if '' is None else 'kept' -> 'kept'; but '' and ''.upper() -> '' (the `and` idiom drops every falsy value)
5. a chain of objects: getattr(u1.address, 'city', None) -> 'Oslo' , getattr(u2.address, 'city', None) -> None
u2.address.city -> AttributeError: 'NoneType' object has no attribute 'city'
6. get chains through dicts: h.get('a', {}).get('b', {}).get('c') -> 1 , with 'x' -> None
h['a']['x']['c'] -> KeyError: 'x'
7. [] is strict, get is lenient: h['zz'] -> KeyError: 'zz' , h.get('zz') -> None
h.get('zz', 'default') -> 'default'
8. no dig: a five-line helper: dig([[1, [2]]], 0, 1, 0) -> 2 , dig([], 0, 1) -> None
dig(h, 'a', 'b', 'c', 'd') -> None (the helper swallows the dead end)
9. no operator to chain: PEP 505's None-aware `?.` was deferred; each step needs its own guard
Try it¶
- Given
users = [{name: "ann", address: {city: "Oslo"}}, {name: "bo"}, {name: "cy", address: nil}], print each user's city ornil/"(none)" three ways: with&., withdig, and with(u[:address] || {}).fetch(:city, "(none)"). - Write
city_of(user)that returns the city or raisesKeyErrorwith your own message when the address is missing, usingfetchwith a block. Then shownil&.length.zero?andnil&.length&.zero?side by side and explain, from the printed error, which call the&.protected.
Solution to 1
safe_navigation_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: each user's city or "(none)", three ways: &., dig, and || {} + fetch.
users = [
{ name: "ann", address: { city: "Oslo" } },
{ name: "bo" },
{ name: "cy", address: nil },
]
puts "1. with &.:"
users.each { |u| puts " #{u[:name]}: #{(u[:address]&.fetch(:city, nil)).inspect}" }
puts "2. with dig:"
users.each { |u| puts " #{u[:name]}: #{u.dig(:address, :city).inspect}" }
puts "3. with || {} and fetch's default:"
users.each { |u| puts " #{u[:name]}: #{(u[:address] || {}).fetch(:city, "(none)")}" }
Verified output of safe_navigation_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Bang means dangerous, question mark means predicate — where a surprise
nilcomes from:strip!and friends returning nil when nothing changed nil,falseand truthiness — whyfalse&.to_sis called:nilandfalseare different objects- Hashes and default values —
fetch,Hash.new(0),digand the default-block idiom in full ||=and nil guards — the other nil idioms:x ||= v,Array(nil),nil.to_a- Common error messages —
undefined method '…' for nilin the library-wide table - Rust: Nullable pointers ↗ — a language that makes "may be absent" a type rather than a value
- Rust: The
?operator ↗ — a similar-looking operator that returns early from the function instead of skipping one call - PEP 505: None-aware operators ↗ — the deferred Python proposal for
?. getsreturns nil at EOF, and readsARGV's files first —getsreturns the next line with its newline andnilat end of input