Assignment destructures, and blocks auto-splat¶
Level: 201 · a Python programmer who unpacks tuples and wants Ruby's rules for the edges
One line: a, b = 1, 2, a, *rest = list and (a, b), c = pair, 3 read like Python — but Ruby fills a missing value with nil and drops an extra one where Python raises ValueError, a String or a Hash on the right stays whole where Python unpacks any iterable, and a block with two parameters splits an Array argument on its own, which a lambda refuses.
Multiple assignment has one mechanism: the right-hand side is collected into an Array — a bare list of values becomes one, an Array stays one, and any other object is asked for to_ary and, failing that, wrapped as a one-element Array — and that Array is then distributed over the targets. Distribution is forgiving: a target with nothing left gets nil, values with no target are dropped, one * target gathers what the others leave, parentheses nest a target list, and a trailing comma (first, = arr) makes a one-target list that takes the first element. Because the whole thing is an expression, it has a value: the collected Array. The same distribution runs when a block receives a single Array and declares more than one parameter — each { |k, v| } over pairs, each_with_index { |(x, y), i| } — which is what makes Hash#each and Hash#map feel like two-argument calls. Blocks and procs auto-splat; lambdas and methods do not, so ->(x, y) { }.call([1, 2]) is an ArgumentError.
Verified output of destructuring_assignment_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. a, b = 1, 2 [1, 2]
2. a, b = b, a (swap) [2, 1]
3. a, *b = [1, 2, 3]; *c, d = [1, 2, 3] [[1, [2, 3]], [[1, 2], 3]]
4. a, *b, c = 1, 2, 3, 4 [1, [2, 3], 4]
5. (a, b), c = [1, 2], 3 (nested) [1, 2, 3]
6. first, = [1, 2] (trailing comma: take the first) 1
7. a, b = [1] (too few: nil) [1, nil]
8. a, b = 1, 2, 3 (too many: dropped) [1, 2]
9. a = 1, 2 (one target: an Array) [1, 2]
10. a, b = "xy" (a String does not splat) ["xy", nil]
11. a, b = {x: 1, y: 2} (nor does a Hash) [{x: 1, y: 2}, nil]
12. a, b = nil [nil, nil]
13. v = (a, b = 1, 2) (the assignment is an expression) [1, 2]
14. pairs.map { |x, y| x + y } (a block auto-splats) [3, 7]
15. pairs.each_with_index.map { |(x, y), i| } ["0:3", "1:7"]
16. {a: 1, b: 2}.map { |k, v| "#{k}#{v}" } ["a1", "b2"]
17. ->(x, y) {}.call([1, 2]); proc { |x, y| }.call([1, 2]); m([1, 2]) ["ArgumentError: wrong number of arguments (given 1, expected 2)", [1, 2], ArgumentError]
18. a, b = obj with to_ary; c, d = a Struct (no to_ary) [[7, 8], [#<struct Point x=1, y=2>, nil]]
19. def two = (return 1, 2); two, then a, b = two [[1, 2], [1, 2]]
Reading the output¶
Rows 1–5 are the forms that match Python exactly: parallel assignment, the swap, * at the start or the end, * in the middle, and nesting with parentheses.
Rows 6–8 are the forgiving distribution. first, = [1, 2] takes 1; a, b = [1] leaves b as nil; a, b = 1, 2, 3 drops the 3. None of them is an error.
Rows 9–12 are the collection step. One target with two values gets the Array [1, 2]. A String, a Hash and nil have no to_ary, so each is wrapped as one value and the second target is nil — a String does not split into characters and a Hash does not split into pairs.
Row 13: the assignment is an expression whose value is the collected Array.
Rows 14–16 are auto-splat. map { |x, y| } over [[1, 2], [3, 4]] receives one Array per call and spreads it; |(x, y), i| nests the spread inside each_with_index's pair; Hash#map yields [key, value] and |k, v| spreads it.
Row 17: a lambda called with one Array and two parameters raises wrong number of arguments (given 1, expected 2); a proc spreads it; a method is as strict as the lambda.
Row 18 is the protocol: an object that defines to_ary destructures; a Struct, which defines to_a but not to_ary, is wrapped whole and d is nil — splat it explicitly (c, d = *point) when you want its members.
Row 19: return 1, 2 returns an Array, so a method has multiple return values only in the sense that the caller destructures one Array.
Compared with Python¶
The twin asks unpacking the same nineteen questions. Rows 1–5 agree, with *rest a list. Then the rules part. Python's unpacking is exact: first, = [1, 2], a, b = [1] and a, b = 1, 2, 3 are all ValueError (rows 6–8), and the way to take a first element is first, *_ = seq. Collection follows the iterator protocol, not to_ary: a = 1, 2 builds a tuple (row 9), a str unpacks into characters and a dict into its keys (rows 10–11), and None is a TypeError because it does not iterate (row 12). Assignment is a statement with no value, so v = (a, b = 1, 2) does not even compile (row 13). for x, y in pairs and for i, (x, y) in enumerate(pairs) are the auto-splat, spelled as unpacking in the loop header (rows 14–16). A lambda — or any function — never spreads a single argument: calling it with a list is a TypeError, tuple parameters (lambda (x, y):) are a SyntaxError since Python 3 removed them (Not machine-checked here.), and itertools.starmap is the spreading call (row 17). The protocol behind rows 18 and 19 is __iter__ and the tuple.
Verified output of destructuring_assignment_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. a, b = 1, 2 [1, 2]
2. a, b = b, a (swap) [2, 1]
3. a, *b = [1, 2, 3]; *c, d = [1, 2, 3] (rest: a list) [[1, [2, 3]], [[1, 2], 3]]
4. a, *b, c = 1, 2, 3, 4 [1, [2, 3], 4]
5. (a, b), c = [1, 2], 3 (nested) [1, 2, 3]
6. first, = [1, 2]; first, = [1]; first, *_ = [1, 2] ValueError, 1, 1
7. a, b = [1] (too few) ValueError
8. a, b = 1, 2, 3 (too many) ValueError
9. a = 1, 2 (one target: a tuple) (1, 2)
10. a, b = "xy" (any iterable unpacks) ['x', 'y']
11. a, b = {'x': 1, 'y': 2} (a dict unpacks its keys) ['x', 'y']
12. a, b = None TypeError
13. compile("v = (a, b = 1, 2)") (a statement: no value) SyntaxError
14. [x + y for x, y in pairs] (for unpacks) [3, 7]
15. for i, (x, y) in enumerate(pairs) ['0:3', '1:7']
16. [f'{k}{v}' for k, v in d.items()] ['a1', 'b2']
17. lambda x, y with [1, 2]; lambda (x, y); starmap [TypeError, SyntaxError, [3, 7]]
18. a, b = obj with __iter__; c, d = obj without [[7, 8], TypeError]
19. def two(): return 1, 2; two(), then a, b = two() [(1, 2), [1, 2]]
Try it¶
- Parse records like
"ada 36 london","bob 41"and"cy 29 paris texas"withname, age, *rest = line.split, printing a missing city as(unknown)and any extra fields separately; then destructure the whole list withfirst, *middle, last, and the pairs thatminmax,divmodandpartitionreturn. - Iterate a Hash with
each_with_index { |(k, v), i| }printingi: k=v, then again witheach.with_index(1), and then write the block as|k, v, i|without the parentheses and explain whatk,vandihold — row 15 and the rule "one Array, more than one parameter" are the hint.
Solution to 1
destructuring_assignment_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: destructure "name age city ..." records with a splat, tolerating missing
# and extra fields -- a missing field is nil, extra fields land in the splat --
# then destructure the pairs that divmod, minmax and partition return.
lines = ["ada 36 london", "bob 41", "cy 29 paris texas"]
lines.each do |line|
name, age, *rest = line.split
city = rest.first || "(unknown)"
extra = rest.drop(1)
printf("%-4s age %-4s city %-10s extra %s\n", name, age.inspect, city, extra.inspect)
end
first, *middle, last = lines
puts "first #{first.inspect}, middle #{middle.inspect}, last #{last.inspect}"
lo, hi = [5, 3, 9].minmax
q, r = 17.divmod(5)
evens, odds = (1..6).partition(&:even?)
puts "minmax #{[lo, hi].inspect}, divmod #{[q, r].inspect}, partition #{[evens, odds].inspect}"
Verified output of destructuring_assignment_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Past the end, an array gives nil, not an error — the splat in a literal and in an argument list, the other direction of
*rest. - Struct is mutable, Data is not —
*pointsplats a Struct throughto_a, and a Data has neitherto_anorto_ary. - A missing key is nil, and
Hash.new([])shares its default —eachyields one[key, value]Array, which|k, v|spreads. - Multiple return values —
return a, bis one Array, destructured at the call site. case/inpattern matching —in [a, [b, c]]and=>do the same distribution with a test attached.- Blocks are not objects — what a block receives, and how
&blockturns it into a Proc. - Procs and lambdas differ — the arity strictness behind row 17.
- Rust: destructuring structs ↗ —
let Point { x, y } = p;, the same idea with the field names as the pattern. - Rust: tuples ↗ — the numbered-field struct that
let (a, b) = pair;takes apart. - Arguments: positional, keyword, splat and block — A Ruby parameter list has eight kinds of slot
- The last expression is the return value — A Ruby method returns the value of the last expression it evaluated, so
returnis only for leaving early itand_1name the block's parameter — A block with no|x|can read its arguments as_1,_2, … (Ruby 2.7) or asit(Ruby 3.4); they are…