Array Methods

Published by Programming Line in Ruby. Last updated .

An array in Ruby is an ordered, indexed list of objects, and almost everything useful you do with one happens through its methods rather than manual index juggling. Ruby ships with a huge, consistent toolkit for transforming, searching, and reshaping arrays, and once you understand the handful of ideas underneath them, methods like map, select, and reduce stop being things to memorize and start feeling inevitable. This lesson walks through the most important Array methods, how they actually work, and the mistakes that catch even experienced Rubyists off guard.

Overview: How Array Methods Work

Every array in Ruby is an instance of the Array class, and Array includes the Enumerable module. That single fact explains most of what you need to know. Enumerable provides dozens of methods, map, select, reject, reduce, sort_by, find, all?, and more, and every one of them is built on top of a single primitive method that Array supplies: each. When you call array.map { |x| x * 2 }, Ruby walks the array element by element (using the same mechanism each uses internally), passes each element into your block, and collects the block’s return values into a brand-new array. This is why so many Enumerable methods feel like variations on a theme: they all iterate the same way, they just do something different with each yielded value.

A second organizing idea is the distinction between methods that return a new array or value and methods that mutate the array in place. By convention, a method whose name ends in ! (a “bang” method) mutates its receiver instead of returning a fresh copy, for example sort returns a new sorted array while sort! reorders the original array and returns it. This is a convention, not a hard rule enforced by the language, but it is followed consistently throughout Ruby’s core library, so you can generally trust it. Because arrays are objects and variables in Ruby hold references to objects rather than copies of them, mutating an array through one variable is visible through every other variable that points at the same array, a detail that matters a great deal once your programs pass arrays into methods or store them in other data structures.

Method lookup for arrays follows Ruby’s ordinary ancestor chain: Ruby first checks the object’s singleton class, then Array itself, then the modules Array includes (such as Enumerable, searched in reverse inclusion order), then Object, and finally Kernel and BasicObject. You can see this chain yourself by calling Array.ancestors. This is also why every array automatically gets dozens of Enumerable methods for free the moment Array defines each; the module supplies the rest.

Syntax

Array methods are called with dot notation, optionally followed by arguments and a block. The general shape looks like this:

array.method_name(arguments) { |element| block_body }
# or, equivalently, with do...end for multi-line blocks
array.method_name(arguments) do |element|
  block_body
end
Method What it does Returns
push / << Appends element(s) to the end The array itself (mutated)
pop / shift Removes the last / first element The removed element
unshift Prepends element(s) to the front The array itself (mutated)
map / collect Transforms each element via a block A new array of results
select / filter Keeps elements where the block is truthy A new array
reject Keeps elements where the block is falsy A new array
reduce / inject Folds all elements into a single value The accumulated value
sort / sort_by Orders elements A new sorted array
uniq Removes duplicate elements A new array
flatten Flattens nested arrays A new, flat array
compact Removes nil elements A new array
each_with_index Iterates yielding element and index The array itself

Most of these methods also have a bang (!) counterpart, sort!, uniq!, reject!, map!, that mutates the receiving array in place instead of returning a new one.

Examples

Example 1: Adding, removing, and inspecting elements

fruits = ["apple", "banana", "cherry"]
fruits.push("date")
fruits << "elderberry"
fruits.unshift("apricot")

puts fruits.inspect
puts "First: #{fruits.first}"
puts "Last: #{fruits.last}"
puts "Length: #{fruits.length}"

removed = fruits.pop
puts "Removed: #{removed}"
puts fruits.inspect

Output:

["apricot", "apple", "banana", "cherry", "date", "elderberry"]
First: apricot
Last: elderberry
Length: 6
Removed: elderberry
["apricot", "apple", "banana", "cherry", "date"]

push and << both append to the end and can be used interchangeably (<< is just a shorter, chainable operator form). unshift adds to the front, which shifts every existing index up by one. pop removes and returns the last element, mutating the array down to five items.

Example 2: Transforming and filtering with map, select, and reduce

prices = [12.50, 8.00, 20.00, 3.20, 45.00]

discounted = prices.map { |price| (price * 0.9).round(2) }
affordable = prices.select { |price| price < 20 }
total = prices.reduce(0) { |sum, price| sum + price }

puts "Discounted: #{discounted}"
puts "Affordable: #{affordable}"
puts "Total: #{total.round(2)}"

Output:

Discounted: [11.25, 7.2, 18.0, 2.88, 40.5]
Affordable: [12.5, 8.0, 3.2]
Total: 88.7

map runs the block on every price and collects the ten-percent-off results into a new array, leaving prices untouched. select keeps only the elements where the block returns a truthy value, here, prices strictly under 20. reduce(0) { |sum, price| sum + price } starts an accumulator at 0 and adds each price to it in turn, ending with the running total.

Example 3: Sorting, deduplicating, and flattening

scores = [42, 17, 42, 8, 99, 17, 3]

unique_sorted = scores.uniq.sort
puts "Unique sorted: #{unique_sorted}"

nested = [[1, 2], [3, [4, 5]], 6]
puts "Flattened: #{nested.flatten}"

words = ["mango", "fig", "banana", "kiwi"]
by_length = words.sort_by { |word| word.length }
puts "By length: #{by_length}"

Output:

Unique sorted: [3, 8, 17, 42, 99]
Flattened: [1, 2, 3, 4, 5, 6]
By length: ["fig", "kiwi", "mango", "banana"]

uniq removes duplicate values while preserving the first occurrence of each, and chaining .sort orders the result numerically. flatten recursively unwraps nested arrays into one flat sequence, no matter how deeply they’re nested. sort_by is different from sort, instead of comparing elements directly, it computes a sort key (here, word.length) for each element once and sorts by that key, which is both clearer and faster than a custom comparison block when you’re sorting by a derived value.

How It Works Step by Step

Consider words.sort_by { |word| word.length } from Example 3. Internally, Ruby does roughly the following: it calls the block once for every element, storing each element paired with its computed key (["mango", 5], ["fig", 3], ["banana", 6], ["kiwi", 4]); it then sorts those pairs by the key using the same comparison logic as sort (which relies on the <=>, or “spaceship,” operator); finally it discards the keys and returns just the elements in their new order. This two-pass approach (compute keys, then sort) is why sort_by only ever calls your block once per element, whereas a plain sort { |a, b| a.length <=> b.length } would call its block on every comparison. map, select, and reject follow a simpler pattern: iterate once via each, run the block on the current element, and either collect the block’s return value (map) or the original element conditionally (select/reject) into a new array that is returned once iteration completes.

Common Mistakes

Mistake 1: Chaining after a bang method that returns nil

Several bang methods, including uniq!, compact!, and flatten!, return nil instead of the array when they don’t actually change anything. Chaining another method directly onto that result blows up:

numbers = [1, 2, 3]
result = numbers.uniq!.push(4)
puts result.inspect

Since [1, 2, 3] has no duplicates, uniq! makes no change and returns nil rather than the array, so calling .push(4) on that nil raises NoMethodError: undefined method 'push' for nil:NilClass. The fix is to call the bang method and the follow-up method as separate statements on the original variable, not as a chain off the bang method’s return value:

numbers = [1, 2, 3]
numbers.uniq!
numbers.push(4)
puts numbers.inspect

Output: [1, 2, 3, 4]. numbers itself is still a valid array regardless of what uniq! returned, so operating on the variable directly sidesteps the problem entirely.

Mistake 2: Mutating an array while iterating over it

Removing elements from an array during an each-style iteration silently skips elements, because the indices shift out from under the loop as items are removed:

arr = [1, 2, 2, 3, 4]
arr.each_with_index do |n, i|
  arr.delete_at(i) if n == 2
end
puts arr.inspect

Output: [1, 2, 3, 4], notice a 2 is still there. Deleting the first 2 at index 1 shifts the second 2 down into index 1, but the loop has already moved on to index 2, so that second 2 is never visited. The safe fix is to build a new array instead of mutating the one you’re iterating, which is exactly what reject! does under the hood without exposing you to the shifting-index problem:

arr = [1, 2, 2, 3, 4]
arr.reject! { |n| n == 2 }
puts arr.inspect

Output: [1, 3, 4]. As a rule, never call a mutating method like delete, delete_at, or push on an array from inside a block that is currently iterating that same array.

Mistake 3: A default value shared across every slot

Array.new(size, default) fills every slot with the exact same object, not separate copies of it. If that default is itself mutable, like an array or hash, every slot ends up pointing at the one shared object:

grid = Array.new(3, [])
grid[0] << "shared"
puts grid.inspect

Output: [["shared"], ["shared"], ["shared"]], even though only grid[0] was appended to, because all three slots are the same array object in memory. The fix is to pass a block instead of a literal default; Ruby calls the block fresh for each slot, producing distinct objects:

grid = Array.new(3) { [] }
grid[0] << "shared"
puts grid.inspect

Output: [["shared"], [], []]. This same shared-reference trap shows up anywhere an object is duplicated by reference rather than by value, so when in doubt, prefer the block form of Array.new for mutable defaults.

Best Practices

  • Reach for map, select, reject, and reduce instead of manual for or index-based loops; they state your intent directly and avoid off-by-one errors.
  • Use the bang (!) version of a method only when you specifically want to mutate the receiver, and remember several bang methods return nil when nothing changed, so never chain off their return value.
  • Prefer Array.new(n) { ... } over Array.new(n, default) whenever the default is a mutable object like an array or hash.
  • Never add or remove elements from an array you are currently iterating with each; use select, reject, or iterate over a duplicate (array.dup.each) if you must delete conditionally.
  • Use sort_by instead of sort with a custom comparison block whenever you’re sorting by a computed key, it’s both clearer and calls your block far fewer times.
  • Remember that in Ruby only nil and false are falsy, so a block like select { |n| n } keeps 0, empty strings, and empty arrays, they are all truthy.

Practice Exercises

  • Given temperatures = [72, 68, 90, 55, 81, 100, 63], use select to build an array of only the temperatures 80 or above, then use map to convert that filtered array from Fahrenheit to Celsius (formula: (f - 32) * 5.0 / 9). Print the rounded results.
  • Given names = ["Sam", "ana", "Sam", "Lee", "ana", "Kim"], write code that prints the unique names in the order they first appeared, without using sort. Hint: think about which non-mutating method removes duplicates while preserving order.
  • Given orders = [["pen", 3], ["book", 12], ["pen", 5]], use reduce to compute the total quantity across all orders (expected output: 20). Then rewrite it using sum with a block instead of reduce, and confirm you get the same answer.

Summary

  • Array includes Enumerable, so methods like map, select, and reduce are all built on the same underlying each iteration.
  • Non-bang methods (sort, uniq, map) return a new array and leave the original untouched; bang methods (sort!, uniq!, map!) mutate the receiver in place.
  • Several bang methods return nil when they make no change, so never chain another method directly off a bang method’s result.
  • Never mutate an array (delete, push) while iterating over that same array with each; use select/reject instead.
  • Array.new(n, default) shares one object across every slot; use Array.new(n) { ... } when the default needs to be a distinct object per slot.
  • sort_by computes a sort key once per element and is generally clearer and faster than sort with a custom comparison block.