skip to content

In Ruby 4.0, how do Array#first, last, take and drop differ in what they return, and what does the new Array#rfind add?

level: juniorimportance: should knowfreq 42%

answer

  1. no argument: element or nil
  2. with a count: always an array
  3. take requires its argument
  4. negative count: ArgumentError
  5. rfind arrived in 4.0

basics

~20 s

first and last with no argument return one element or nil; with a count they, like take and drop, return a new array and never nil. Ruby 4.0 adds Array#rfind, which returns the last element matching a block.

solid answer

~40 s

`first` and `last` without an argument return a **single element**, or `nil` on an empty array. With a count, `first(2)` and `last(2)` return a **new array** of up to that many elements, `[]` when the array is empty. `take(n)` returns the first `n` elements like `first(n)` but requires its argument; `drop(n)` returns everything **except** the first `n`. None of them modify the receiver. Oversized counts are clamped, and negative counts raise `ArgumentError` (`negative array size` from `first`, `attempt to take negative size` from `take`). Ruby 4.0 added `Array#rfind`, which scans from the end and returns the **last** element for which the block is truthy, or `nil`; it is a more efficient alternative to `reverse_each.find`.

code

ruby · 17 lines
ruby
row = %w[A1 A2 A3 A4]

row.first      # => "A1"
row.first(2)   # => ["A1", "A2"]
row.last(2)    # => ["A3", "A4"]
row.take(10)   # => ["A1", "A2", "A3", "A4"]
row.drop(3)    # => ["A4"]
row            # => ["A1", "A2", "A3", "A4"]

[].first       # => nil
[].first(1)    # => []
row.take(-1)   # ArgumentError: attempt to take negative size

# Ruby 4.0
seats = %w[A1 B1 A2 B2]
seats.rfind { |s| s.start_with?("A") }  # => "A2"
seats.rindex { |s| s.start_with?("A") } # => 2

go deeper

for a junior

Recall that a count argument turns first and last into array-returning methods, and that drop returns the remainder, not the dropped part.

for a middle

Explain nil versus empty-array returns, clamping of large counts and the ArgumentError on negative ones, and that none of these methods mutate.

for a senior

Pick the form whose empty-array behaviour suits the caller, and adopt rfind only where every deployment runs Ruby 4.0.

for a principal

Plan when new core methods like rfind can be used across services, since code relying on them fails on any app still on Ruby 3.4.

## One element or an array The methods that read from the ends of a Ruby array split on one question: **did you pass a count?** | Call | Returns | On an empty array | |---|---|---| | `row.first` | one element | `nil` | | `row.first(2)` | new array of up to 2 elements | `[]` | | `row.last` | one element | `nil` | | `row.last(2)` | new array of up to 2 elements | `[]` | | `row.take(2)` | new array, same as `first(2)` | `[]` | | `row.drop(2)` | new array without the first 2 | `[]` | The difference in return type matters. `row.first.upcase` raises `NoMethodError` on an empty row because `first` returned `nil`, while `row.first(1).each` simply does nothing. ## take and drop - **`take(n)`** returns the first `n` elements. It behaves like `first(n)` but its argument is required. - **`drop(n)`** is the complement: all elements after the first `n`. `%w[A1 A2 A3].drop(1)` is `["A2", "A3"]`. `take` and `drop` are also defined by `Enumerable`, so the same names work on ranges and other collections. ## The edges 1. **Count larger than the array**: clamped. `row.first(50)` and `row.take(50)` return the whole array. 2. **Count of zero**: `[]`. 3. **Negative count**: `ArgumentError`. `first(-1)` and `last(-1)` report `negative array size`; `take(-1)` reports `attempt to take negative size`; `drop(-1)` reports `attempt to drop negative size`. A negative count does not mean "from the end". 4. **Receiver**: never modified. `drop(1)` is not `shift`; the original array keeps all its elements. ## Array#rfind in Ruby 4.0 Before Ruby 4.0, finding the **last** element matching a condition meant `row.reverse.find { ... }`, which copies the array, or `row.reverse_each.find { ... }`, which goes through an enumerator. Ruby 4.0 added `Array#rfind` as a more efficient alternative: - It calls the block with elements **from the end backwards** and returns the first one for which the block is truthy, which is the last match in array order. - With no match it returns `nil`, or the result of an optional `if_none_proc` argument: `row.rfind(proc { :none }) { ... }`. - Without a block it returns an `Enumerator`. It returns an **element**, not an index; `rindex` is the method that returns the index of the last match. Ruby 4.0 also gave `Array` its own `find`, a faster override of `Enumerable#find`, so `find` and `rfind` are now a matched pair on arrays. ## Finding the last match: the options compared | Expression | Returns | Extra work | |---|---|---| | `row.reverse.find { ... }` | last matching element | builds a reversed copy of the array | | `row.reverse_each.find { ... }` | last matching element | goes through an enumerator | | `row.rfind { ... }` (4.0) | last matching element | none, walks from the end | | `row.rindex { ... }` | index of the last match | none | On a codebase that must still run on Ruby 3.4, keep `reverse_each.find`; calling `rfind` there raises `NoMethodError`. ## Counts that are not integers The count is converted to an integer, so `row.take(2.1)` behaves like `row.take(2)`. Passing `nil` or a string raises `TypeError`, which usually signals a count that came from unparsed input. ## Choosing in a theatre booking app - The first seat in a row: `row.first`, and handle `nil` for an empty row. - The first three seats to show in a preview: `row.first(3)`, which never returns `nil`. - Everything after the aisle seat: `row.drop(1)`. - The last seat in a row not yet booked: `row.rfind { |seat| !booked.include?(seat) }` on Ruby 4.0.

  • Why can code that works with row.first(1) fail when changed to row.first?
    `first(1)` always returns an array, `[]` for an empty row, so chained calls like `.each` or `.map` just do nothing. `first` with no argument returns the element itself or `nil`, so a following method call such as `.upcase` raises `NoMethodError` on an empty row.
  • How is Array#rfind different from rindex and from reverse.find?
    `rfind` returns the last matching **element**; `rindex` returns its **index**. `reverse.find` gives the same element as `rfind` but first builds a reversed copy of the whole array. `rfind` walks from the end directly. It exists only on Ruby 4.0 and later; on 3.4 use `reverse_each.find`.

saying these in an interview costs you the question

  • first(1) returns the element itself, just like first
  • take removes the elements it returns from the array
  • drop(2) returns the two elements it dropped
  • first(-1) returns the last element of the array
  • rfind returns the index of the last match, like rindex
  • Array#rfind has been available since Ruby 3.x