A variable is a reference, not a box¶
Level: 101 · anyone who has been surprised that changing b changed a
One line: Assignment copies a reference, never the object, so after b = a a mutating method such as << on b is seen through a, while b = b + x and b += x bind b to a new object and leave a alone — Python has the same model with one exception: += on a list mutates it in place.
A Ruby variable holds a reference to an object; the object lives elsewhere and can have any number of names at once. There are two different things you can do with a name, and everything on this page follows from telling them apart. You can mutate the object — <<, push, upcase!, []= — and every name that refers to it sees the change. Or you can rebind the name — =, and +=, which in Ruby is never anything but b = b + x — and no other name notices. A method's parameter is a name too, so a method can change the caller's object but cannot change what the caller's variable refers to. When you want an object of your own, dup makes a new one, one level deep.
Verified output of variables_are_references_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. two names, one object
a = [1]; b = a; b << 2; a [1, 2]
b [1, 2]
a.equal?(b) true
2. + makes a new object; = rebinds the name
b = b + [3]; a [1, 2]
b [1, 2, 3]
a.equal?(b) false
3. += is always a rebinding in Ruby
b = a; b += [4]; a [1, 2]
b [1, 2, 4]
a.equal?(b) false
4. the same with a string
s = "x"; t = s; t << "y"; s "xy"
t += "z"; s "xy"
t "xyz"
s.equal?(t) false
5. a method receives the reference
push_four(orig); orig [1, 4]
reassign(orig) returns [9]
orig after reassign [1, 4]
shout(word); word (upcase! in place) "HI"
6. dup breaks the sharing, one level deep
base = [1, 2]; c = base.dup; c.equal?(base) false
c << 9; base [1, 2]
c [1, 2, 9]
nested = [[1]]; nested.dup[0] << 2; nested [[1, 2]]
Marshal round trip, then [0] << 3; nested [[1, 2]]
7. the shared-default trap
Array.new(3, "x"), then [0] << "!" ["x!", "x!", "x!"]
Array.new(3) { "x" }, then [0] << "!" ["x!", "x", "x"]
Reading the output¶
- Row 1. After
b = athere is one array with two names.<<throughbis seen througha, andequal?confirms they are one object. - Row 2.
+builds a new array and=bindsbto it;astill refers to the old one, andequal?is nowfalse. - Row 3.
+=isb = b + [4], a rebinding, soais untouched. Ruby has no in-place+=. - Row 4. The same two operations on a string:
<<changes the shared object (ssees"xy"),+=rebindstto a new string (sstays"xy",tbecomes"xyz"). - Row 5.
push_fourmutates the caller's array;reassignrebinds only its own parameter, soorigis unchanged and the new array comes back only as the return value;upcase!changes the caller's string in place. - Row 6.
dupcopies one level:cis independent ofbase, but the inner array ofnested.dupis still the same object asnested[0]. AMarshalround trip is the stdlib deep copy. - Row 7.
Array.new(3, "x")is one string three times over; the block formArray.new(3) { "x" }evaluates the block once per slot.
Compared with Python¶
Python has the same model — its FAQ calls it call by object reference (Not machine-checked here.) — and rows 1, 2, 5, 6 and 7 print the same answers: assignment shares, + and = make a new list and rebind, a function sees the caller's list but cannot rebind the caller's name, copy.copy is shallow and copy.deepcopy is deep, and [[]] * 3 is the same trap as Array.new(3, "x"). Row 3 is the difference: b += [4] on a list calls list.__iadd__, which extends the list in place and returns the same object, so a changes too and a is b stays True; in Ruby += is a rebinding on every type. Row 4 is a difference of the opposite kind: a str is immutable, so there is no <<, t[0] = "y" is a TypeError, and += can only rebind — which is why "strings are safe to share" is true in Python by construction and true in Ruby only for frozen strings.
Verified output of variables_are_references_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. two names, one object
a = [1]; b = a; b.append(2); a [1, 2]
b [1, 2]
a is b True
2. + makes a new object; = rebinds the name
b = b + [3]; a [1, 2]
b [1, 2, 3]
a is b False
3. += on a list mutates in place in Python
b = a; b += [4]; a [1, 2, 4]
b [1, 2, 4]
a is b True
4. the same with a string: no << exists
s = "x"; t = s; t[0] = "y" TypeError
t += "y"; s 'x'
t 'xy'
s is t False
5. a function receives the reference
push_four(orig); orig [1, 4]
reassign(orig) returns [9]
orig after reassign [1, 4]
shout(word); word (upper() returns new) 'hi'
6. copy.copy breaks the sharing, one level deep
base = [1, 2]; c = copy.copy(base); c is base False
c.append(9); base [1, 2]
c [1, 2, 9]
nested = [[1]]; copy.copy(nested)[0].append(2) [[1, 2]]
copy.deepcopy, then [0].append(3); nested [[1, 2]]
7. the shared-default trap
[[]] * 3, then [0].append('!') [['!'], ['!'], ['!']]
[[] for _ in range(3)], then the same [['!'], [], []]
The Python FAQ's entry on writing a function with output parameters ↗ states the rule for functions; the copy module ↗ documents the shallow and deep copies of row 6.
Try it¶
- Write
sharing?(a, b)that returnsa.equal?(b), then tracea = [1]; b = a,b << 2,b += [3],b = a.dupandb << 4, printinga,bandsharing?(a, b)after every step. At which step doesbstop being a name fora's object? - Build a 3×3 grid with
Array.new(3, Array.new(3, 0)), setgrid[0][0] = 1and print the grid. Explain what you see, fix it with the block form, and printgrid.map(&:object_id).uniq.sizebefore and after — a count is stable where the ids themselves are not.
Solution to 1
variables_are_references_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: trace whether a and b share one object after each step. equal? is the
# identity test; << mutates the shared object, += and dup make a new one.
def sharing?(a, b) = a.equal?(b)
def show(step, a, b)
puts " #{step.ljust(12)} a = #{a.inspect.ljust(12)} b = #{b.inspect.ljust(12)} sharing? #{sharing?(a, b)}"
end
a = [1]
b = a
show "b = a", a, b
b << 2
show "b << 2", a, b
b += [3]
show "b += [3]", a, b
b = a.dup
show "b = a.dup", a, b
b << 4
show "b << 4", a, b
Verified output of variables_are_references_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Strings are mutable;
<<keeps the object — row 4 as a whole page: whichStringmethods mutate and which return a new string freezeis shallow and raisesFrozenError— how to make a shared object safe to sharedup,cloneand frozen state — row 6 in depth: what each copy keeps, and theMarshaldeep copy- Arguments: positional, keyword and splat — how a method receives the references of row 5
- Arrays and negative indexes — row 7's
Array.new(3, [])trap where it usually bites - Assignment ↗ — the Ruby 4.0 syntax page; copy ↗ — Python's shallow and deep copies
- Default arguments are evaluated on every call — A Ruby default is an expression that runs each time the method is called without that argument, so `def…
- The GC answers comparisons, not counts —
GC.start,GC.stat,ObjectSpaceandWeakReflet a program watch its own heap, but only comparisons…