Ruby Array Methods: The Practical Guide
Ruby arrays come with a powerful set of built-in methods. Most real-world code reaches for the same handful over and over. This guide covers those: the methods that turn messy data into clean results.
map
map transforms every element in an array and returns a new array with the results. The original array stays untouched.
numbers = [1, 2, 3]
doubled = numbers.map { |n| n * 2 }
# => [2, 4, 6]
doubled
# => [2, 4, 6]
numbers
# => [1, 2, 3]
Return value: A new array with each element replaced by the block’s return value.
Gotcha: map always returns an array, even if the block returns nil or mixes types:
[1, 2, 3].map { |n| n.odd? ? n : nil }
# => [1, nil, 3]
If you chain map off an each or forget to use map, you lose the transformation:
# WRONG — map is silently ignored:
[1, 2, 3].each { |n| n * 2 }
# => [1, 2, 3] (original, no transformation happened)
select and reject
select keeps elements where the block returns truthy. reject does the opposite — it keeps elements where the block returns falsy.
numbers = [1, 2, 3, 4, 5]
numbers.select(&:odd?)
# => [1, 3, 5]
numbers.reject(&:odd?)
# => [2, 4]
Return value: A new array. Neither method mutates the original.
Common gotcha: Both always return an array — even when empty:
[1, 2, 3].select { |n| n > 99 }
# => [] (empty array, not nil)
This means select is safe to chain without worrying about nil. The same applies to reject.
select and reject are also available as filter and filter_reject if that reads more naturally in your code.
reduce
reduce accumulates all elements into a single value. You provide an initial accumulator that gets passed through each iteration.
[1, 2, 3, 4].reduce(0) { |acc, n| acc + n }
# => 10
# With no initial value, the first element becomes the accumulator:
[1, 2, 3, 4].reduce { |acc, n| acc + n }
# => 10
Return value: A single value of whatever type your accumulator starts as.
Common gotcha: Forgetting the initial value when your block expects a specific type:
# WRONG — starts with string "0" but tries to call + on an integer:
["a", "b", "c"].reduce { |acc, n| acc + n }
# => TypeError: String can't be coerced into Integer
# RIGHT — start with empty string:
["a", "b", "c"].reduce("") { |acc, n| acc + n }
# => "abc"
reduce is also available as inject — both do exactly the same thing.
find
find returns the first element that matches your condition. If nothing matches, it returns nil.
users = [
{ name: "Alice", role: "dev" },
{ name: "Bob", role: "admin" },
{ name: "Carol", role: "dev" }
]
users.find { |u| u[:role] == "admin" }
# => {:name=>"Bob", :role=>"admin"}
users.find { |u| u[:role] == "superuser" }
# => nil
Return value: The matching element, or nil if nothing matches.
Gotcha: Because find can return nil, chaining off it without guards causes NoMethodError:
# DANGEROUS — crashes if no admin exists:
users.find { |u| u[:role] == "admin" }[:email]
# => NoMethodError: undefined method `[]' for nil:NilClass
# SAFE — use &. for optional chaining:
users.find { |u| u[:role] == "admin" }&.[](:email)
# => nil (if no admin)
The safe navigation operator &. is your friend with find.
sort_by
sort_by sorts elements based on a value you extract from each one. It’s cleaner and faster than using sort with a manual comparison block.
words = ["banana", "apple", "cherry", "date"]
words.sort_by(&:length)
# => ["date", "apple", "banana", "cherry"]
words.sort_by { |w| w.chars.first }
# => ["apple", "banana", "cherry", "date"] (alphabetical by first letter)
Return value: A new sorted array. The original is unchanged.
Gotcha: Always use sort_by over sort when comparing computed values. With sort, your comparison block runs O(n²) times; with sort_by, the key extraction runs O(n) times:
# Slow — calls downcase twice per comparison:
words.sort { |a, b| a.downcase <=> b.downcase }
# Fast — calls downcase once per element:
words.sort_by(&:downcase)
uniq
uniq removes duplicate elements, preserving the order of first appearances.
[1, 2, 2, 3, 1, 4, 3].uniq
# => [1, 2, 3, 4]
Return value: A new array without duplicates.
Gotcha — the bang variant is tricky:
a = [1, 2, 3]
b = a.uniq!
# => nil (no changes needed, already unique)
b
# => nil # NOT the array — nil!
a
# => [1, 2, 3] # original unchanged too
uniq! returns nil when no duplicates were removed. This is different from most bang methods. Never write arr.uniq! || arr — it always returns arr even when uniq! actually worked.
flatten
flatten collapses nested arrays into a single flat array.
nested = [[1, 2], [3, [4, 5]], 6]
nested.flatten
# => [1, 2, 3, 4, 5, 6]
# With a depth argument:
nested.flatten(1)
# => [1, 2, 3, [4, 5], 6]
Return value: A new flat array.
Gotcha: flatten only flattens arrays — it doesn’t recursively flatten hashes or other nested structures:
[[1, 2], { a: 3 }].flatten
# => [1, 2, {:a=>3}] # hash stays as-is
For deeply nested or irregular structures, consider writing a recursive custom method instead.
Chaining Methods
One of Ruby’s strengths is that most array methods return arrays, which means you can chain them together cleanly.
# Get squared values of even numbers from 1 to 10
(1..10).to_a
.select(&:even?)
.map { |n| n ** 2 }
# => [4, 16, 36, 64, 100]
# Find the first long word and upcase it
words = ["cat", "elephant", "dog", "rhinoceros"]
words
.select { |w| w.length > 4 }
.find { |w| w.start_with?("r") }
&.upcase
# => "RHINOCEROS"
# Sort users by age, extract their names as symbols
users = [
{ name: "Carol", age: 34 },
{ name: "Alice", age: 28 },
{ name: "Bob", age: 41 }
]
users
.sort_by { |u| u[:age] }
.map { |u| u[:name].to_sym }
# => [:Alice, :Carol, :Bob]
The key insight: select, reject, map, uniq, flatten all return arrays and accept arrays — so they chain in any order. find breaks the chain because it returns a single element or nil, so always put it at the end or guard it with &..
Mutating vs Non-Mutating
Most array methods return a new array. The bang (!) variants mutate the original. Here’s the breakdown for the methods in this guide:
| Method | Non-mutating | Mutating |
|---|---|---|
sort_by | sort_by { } | sort_by! { } |
uniq | uniq | uniq! |
flatten | flatten | flatten! |
select | select { } | — |
reject | reject { } | — |
map | map { } | — |
reduce | reduce | — |
find | find { } | — |
select, reject, map, reduce, and find have no mutating bang variants. If you need to mutate, assign the result back: numbers = numbers.map { |n| n * 2 }.
Note: uniq! is an exception — it returns nil when no changes are made, unlike other bang methods that return the mutated array.
Common Patterns
Filter, Transform, Deduplicate
Extract usernames from admin users, downcase them, remove duplicates:
users = [
{ name: "Alice", roles: ["admin", "dev"] },
{ name: "Bob", roles: ["dev"] },
{ name: "CAROL", roles: ["admin", "dev"] }
]
users
.select { |u| u[:roles].include?("admin") }
.map { |u| u[:name].downcase }
.uniq
# => ["alice", "carol"]
Build a Frequency Map with Reduce
Count how many times each word appears:
words = %w[apple banana apple cherry banana apple]
frequency = words.reduce(Hash.new(0)) do |counts, word|
counts[word] += 1
counts
end
# => {"apple"=>3, "banana"=>2, "cherry"=>1}
Hash.new(0) creates a hash where missing keys default to 0, making the counting logic clean.
Safe Nested Extraction
Find a user and safely dig into nested attributes:
users = [
{ id: 1, metadata: { email: "alice@example.com" } },
{ id: 2, metadata: {} }
]
# Find user 1 and get their email, without crashing on nil metadata
users
.find { |u| u[:id] == 1 }
&.dig(:metadata, :email)
# => "alice@example.com"
users
.find { |u| u[:id] == 99 }
&.dig(:metadata, :email)
# => nil (no crash, no user 99)
&. (safe navigation) combined with dig handles missing keys gracefully.
See Also
- /reference/array-methods/map/ — Transform elements with
map - /reference/array-methods/select/ — Filter elements with
select - /reference/array-methods/reduce/ — Accumulate with
reduce - /reference/array-methods/sort/ — Sorting in Ruby
- /reference/array-methods/uniq/ — Remove duplicates with
uniq