Past the end, an array gives nil, not an error¶
Level: 101 · a Python programmer's first week with Ruby arrays
One line: a[10] on a four-element array is nil, a[5] = 99 pads the gap with nil, and a slice is a[1..2] (end included) or a[1, 2] (start and length) — where Python raises IndexError on both the read and the write and has one slice form, a[1:3], whose stop is excluded.
A Ruby Array is the same kind of thing as a Python list: an ordered, growable, heterogeneous sequence, indexed from zero, with negative indexes counting from the end. The contracts differ at the edges. Reading past either end returns nil rather than raising; fetch is the method that raises, and it takes a default. Writing past the end grows the array and fills the gap with nil — but writing before the start (d[-5] = 1 on four elements) is an IndexError, because there is no gap to fill there. A slice has two spellings, a[start..end] with a Range and a[start, length], and its own edge rule: a slice that starts exactly at the end is [], one that starts past it is nil. Two more things a Python reader should carry over: Array.new(3, []) stores one default object in all three slots, exactly as [[]] * 3 does, and the block form Array.new(3) { [] } is the fresh-object form, exactly as the comprehension is; and push, << and unshift return the array, so they chain, where Python's append and insert return None.
Verified output of arrays_and_negative_indexes_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. a = [10, 20, 30, 40]; a.length 4
2. a[0], a[-1] (negative: from the end) [10, 40]
3. a[10], a[-10] (past either end) [nil, nil]
4. a.fetch(10) (the raising form) IndexError: index 10 outside of array bounds: -4...4
5. a.fetch(10, :none) :none
6. a[1..2] (two dots: end included) [20, 30]
7. a[1...3] (three dots: end excluded) [20, 30]
8. a[1..] (endless range: to the end) [20, 30, 40]
9. a[1, 2] (start, length) [20, 30]
10. a[4..], a[5..] (slice at / past the end) [[], nil]
11. b[5] = 99 on a 4-element copy (pads) [10, 20, 30, 40, nil, 99]
12. d = a.dup; d[-5] = 1 (before the start) IndexError: index -5 too small for array; minimum: -4
13. a.first(2), a.last(2), [].first [[10, 20], [30, 40], nil]
14. a.values_at(0, 2, 10) [10, 30, nil]
15. Array.new(3, []) then x[0] << 1 (shared) [[1], [1], [1]]
16. Array.new(3) { [] } then y[0] << 1 (fresh) [[1], [], []]
17. nested.flatten, nested.flatten(1) [[1, 2, 3, 4], [1, 2, [3, [4]]]]
18. c = [1, 2]; c.push(3) << 4 (returns c) [1, 2, 3, 4]
19. c.pop, c.shift, then c [4, 1, [2, 3]]
20. c.unshift(0) (returns c) [0, 2, 3]
21. [*a, 5], [*1..3], f(*a) with def f(*args) [[10, 20, 30, 40, 5], [1, 2, 3], [10, 20, 30, 40]]
22. a - [20], a & [20, 99], a | [99] [[10, 30, 40], [20], [10, 20, 30, 40, 99]]
Reading the output¶
Rows 1–5 are indexing. a[-1] is the last element, a[10] and a[-10] are both nil, and fetch(10) is the raising form, whose message names the bounds -4...4; fetch(10, :none) returns the default instead.
Rows 6–10 are slicing. a[1..2] and a[1...3] are the same two elements, because two dots include the end and three exclude it; a[1..] runs to the end; a[1, 2] means "two elements starting at 1", not "1 to 2". Row 10 is the edge rule: a slice starting at index 4 of a four-element array is [], and one starting at 5 is nil.
Rows 11–12 are writing. Assigning to index 5 grows the copy to six elements with a nil in slot 4; assigning to index −5 of a fresh four-element copy raises, with a message that names the minimum index.
Rows 13–14: first(n) and last(n) are the slices you reach for most, [].first is nil like every other miss, and values_at picks several indexes at once, returning nil for a missing one.
Rows 15–16 are the shared-default trap. Array.new(3, []) evaluates [] once and stores that one array three times, so appending through x[0] shows up in every slot. Array.new(3) { [] } runs the block once per slot.
Row 17: flatten recurses all the way down and flatten(1) one level.
Rows 18–20: push (and its alias <<) and unshift return the array, so c.push(3) << 4 is one expression; pop and shift return the element they removed.
Row 21: * splats an array into a literal or an argument list, and def f(*args) collects the arguments back into an Array. Row 22: -, & and | are set-like operators on arrays, keeping the left operand's order.
Compared with Python¶
The Python twin asks the same twenty-two questions of a list. The reads and writes that gave nil or padded in Ruby raise IndexError in Python (rows 3, 4, 11, 12, 13 and 14), so there is no fetch — plain indexing already is the raising form, and a default needs an if (row 5). Slicing has one spelling, a[start:stop] with the stop excluded (rows 6–9), and a slice that starts anywhere past the end is [], never None (row 10). The shared-default trap is the same trap under a different spelling, [[]] * 3 (rows 15–16). Python has no flatten (row 17: a recursive helper, or itertools.chain.from_iterable for one level). append, extend and insert return None, so they do not chain (rows 18 and 20), and *args collects into a tuple, not a list (row 21). list - list is a TypeError; the set-like operators need a set (row 22).
Verified output of arrays_and_negative_indexes_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. a = [10, 20, 30, 40]; len(a) 4
2. a[0], a[-1] (negative: from the end) [10, 40]
3. a[10], a[-10] (past either end) IndexError, IndexError
4. a[10] is already the raising form IndexError
5. a[10] if len(a) > 10 else 'none' 'none'
6. a[1:3] (stop excluded; no inclusive form) [20, 30]
7. a[1:3] (the only slice form) [20, 30]
8. a[1:] (to the end) [20, 30, 40]
9. a[1:1 + 2] (no start, length form) [20, 30]
10. a[4:], a[5:] (slice at / past the end) [[], []]
11. b[5] = 99 on a 4-element copy IndexError
12. d = a[:]; d[-5] = 1 (before the start) IndexError
13. a[:2], a[-2:], [][0] [10, 20], [30, 40], IndexError
14. itemgetter(0, 2)(a); with 10 too (10, 30), IndexError
15. [[]] * 3 then x[0].append(1) (shared) [[1], [1], [1]]
16. [[] for _ in range(3)] then y[0]... (fresh) [[1], [], []]
17. flatten(nested) helper, chain one level [[1, 2, 3, 4], [1, 2, [3, [4]]]]
18. c = [1, 2]; c.append(3), c.extend([4]), c [None, None, [1, 2, 3, 4]]
19. c.pop(), c.pop(0), then c [4, 1, [2, 3]]
20. c.insert(0, 0), then c [None, [0, 2, 3]]
21. [*a, 5], [*range(1, 4)], f(*a) with *args [[10, 20, 30, 40, 5], [1, 2, 3], (10, 20, 30, 40)]
22. a - [20]; sorted(set(a) - {20}) TypeError, [10, 30, 40]
The Python library's Slicing is not indexing ↗ measures the same two contracts on str — s[n] may raise, s[:n] may not — and what that does to text.
Try it¶
- Write
rotate_right(a, k)that returns a copy ofarotated right bykplaces using only negative-index slices and the splat — noArray#rotate— and make it work fork = 0, forklarger than the size and for negativek. Check every result againsta.rotate(-k). - Write
chunks(a, n)that splits an array into arrays of at mostnelements witha[i, n]in a loop that stops when the slice comes backnil, then compare its result witha.each_slice(n).to_a— and explain why the loop must test fornilrather than for[](row 10 is the hint).
Solution to 1
arrays_and_negative_indexes_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: rotate an array right by k places with negative slices and the splat,
# for any integer k, without Array#rotate -- then check against a.rotate(-k).
def rotate_right(a, k)
return a.dup if a.empty?
k %= a.size # 0, k > size and negative k all reduce to 0...size
[*a[-k..], *a[...-k]] # the last k elements first, then everything before them
end
a = [1, 2, 3, 4, 5]
(-2..7).each do |k|
mine = rotate_right(a, k)
verdict = mine == a.rotate(-k) ? "== a.rotate(-k)" : "DIFFERS from a.rotate(-k)"
printf("k = %2d %-16s %s\n", k, mine.inspect, verdict)
end
puts "empty: #{rotate_right([], 3).inspect}"
Verified output of arrays_and_negative_indexes_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
k = -2 [3, 4, 5, 1, 2] == a.rotate(-k)
k = -1 [2, 3, 4, 5, 1] == a.rotate(-k)
k = 0 [1, 2, 3, 4, 5] == a.rotate(-k)
k = 1 [5, 1, 2, 3, 4] == a.rotate(-k)
k = 2 [4, 5, 1, 2, 3] == a.rotate(-k)
k = 3 [3, 4, 5, 1, 2] == a.rotate(-k)
k = 4 [2, 3, 4, 5, 1] == a.rotate(-k)
k = 5 [1, 2, 3, 4, 5] == a.rotate(-k)
k = 6 [5, 1, 2, 3, 4] == a.rotate(-k)
k = 7 [4, 5, 1, 2, 3] == a.rotate(-k)
empty: []
See also¶
- Two dots include the end, three dots exclude it — the Range objects behind
a[1..2],a[1...3]and the endlessa[1..]. - A missing key is nil, and
Hash.new([])shares its default — the Hash has the same shared-default trap asArray.new(3, []). - Assignment destructures, and blocks auto-splat —
first, *rest = ais the other side of the splat in row 21. - Arguments: positional, keyword and splat — what
def f(*args)collects, and what**and&collect. - The Enumerable toolbox —
each_slice,zip,flat_mapand the rest of what an Array can do beyond indexing. - Gotchas for Python programmers —
a[10]isnilis one row of that list. - Slicing is not indexing ↗ — the Python library on the two contracts, for strings.
- Rust:
Vec↗ — the growable array as three numbers and one heap allocation. - A variable is a reference, not a box — Assignment copies a reference, never the object, so after
b = aa mutating method such as<<onbis… - Set is a core class in 4.0 and keeps insertion order — In Ruby 4.0
Setneeds norequireand is written in C,Set[3, 1, 2]iterates as 3, 1, 2, and an… - Common error messages — Twenty-nine errors you will meet in your first month of Ruby, each with the code that raises it, the…
Random.new(42)gives the same numbers every run — A seededRandomis a function of its seed