Ruby array methods: map, select, reduce, and more
Ruby arrays come with a powerful set of built-in methods, and most real-world code reaches for the same handful over and over. This guide covers seven Ruby array methods that turn messy data into clean results: map, select, reduce, find, sort_by, uniq, and flatten. Each section explains the return value, a common gotcha, and why you would choose one method over another.
TL;DR
The most useful array methods in Ruby are map, select, reduce, find, sort_by, uniq, and flatten. Use map to transform every element, select to keep matching items, reduce to collapse many values into one, and find when you only need the first match. sort_by, uniq, and flatten help with ordering, deduplication, and structure cleanup. The examples below stay focused on the return value so you can see exactly how each method changes the array.
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. This is different from each, which returns the original array regardless of what the block does:
[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 entirely because each ignores the block’s return value. Here is a common mistake where the developer meant to double each number but used each instead of map:
# WRONG, map is silently ignored:
[1, 2, 3].each { |n| n * 2 }
# => [1, 2, 3], original, no transformation happened
map is the right choice when every element needs a new value. If you only want to inspect items without changing them, each is fine. The important difference is that map creates a result you can keep using, while each is for side effects. That distinction shows up constantly in Ruby code, so it is worth noticing early.
select and reject
select keeps elements where the block returns truthy. reject does the opposite: it keeps elements where the block returns falsy. Together they cover the two most common filtering directions in a single mental model.
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 array; the input stays exactly as it was before the call. This immutability makes both methods safe to use inside loops and across method boundaries where you do not want side effects leaking into shared data structures.
Common gotcha: Both methods always return an array, even when no elements match the condition:
[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. If your downstream code expects an array, an empty one is usually much safer than nil because iteration and chaining work without guards.
select and reject are also available as filter and filter_reject if that reads more naturally in your code.
When you reach for these methods, think about intent instead of syntax. select answers “which items should stay?” and reject answers “which items should go away?” That framing keeps the block simple and makes the code easier to review later. A reader should be able to guess the result just by looking at the condition.
reduce
reduce accumulates all elements into a single value. You provide an initial accumulator that gets passed through each iteration, and the block decides how the accumulator changes with each element.
[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. The type of the initial value determines the type of the result: start with 0 for a sum, "" for concatenation, or {} for building a hash.
Common gotcha: Forgetting the initial value when your block expects a specific type can cause a TypeError. The first element becomes the accumulator if you omit the initial value, which works for simple cases but fails when the types do not match:
# WRONG, starts with string "a" then tries to add an integer:
["a", "b", "c"].reduce { |acc, n| acc + n }
# => "abc" (surprisingly works for strings)
# WRONG for numeric operations, first element is a string:
["a", "b", "c"].reduce(0) { |acc, n| acc + n }
# => TypeError: String can't be coerced into Integer
# RIGHT, start with empty string for concatenation:
["a", "b", "c"].reduce("") { |acc, n| acc + n }
# => "abc"
reduce is also available as inject. Both do exactly the same thing and the choice is purely stylistic.
reduce is the method to choose when the array is just the starting point for a larger result. That result might be a sum, a string, a hash, or a more complex accumulator. The block should explain how one step changes the running total, not how to process every possible edge case at once. Keeping that mental model small makes reduce much easier to read.
find
find returns the first element that matches your condition. If nothing matches, it returns nil. It stops scanning as soon as it finds a match, which makes it faster than select when you only need one result.
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. Because the return can be nil, any code that calls a method on the result must guard against the nil case.
Gotcha: Chaining off find without guards causes NoMethodError. Always protect the chain with the safe navigation operator &. which short-circuits to nil when the receiver is nil:
# 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. It short-circuits to nil when the receiver is nil, preventing the chain from blowing up on a missing value.
The real strength of find is that it stops as soon as it finds a match. That makes it a good fit for “first one wins” problems, such as locating a user, a setting, or a record in a small collection. If you need every match, select is the better tool. If you only need the first match, find keeps the code shorter and often more direct.
sort_by
sort_by sorts elements based on a value you extract from each one. It is cleaner and faster than using sort with a manual comparison block because it calls the extraction block once per element instead of once per comparison.
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. sort_by is one of the array methods that always returns a new array, which means you can chain it with other non-mutating methods safely.
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)
The key advantage is that you describe the sort key once per element, then let Ruby handle the ordering. That is easier to reason about than comparing two values over and over in a custom block. When the sort key is simple, sort_by also tends to be easier to skim because the block shows the data you care about, not the mechanics of comparison.
uniq
uniq removes duplicate elements while preserving the order of first appearances. It is the method you reach for when you have a list and need each value to appear exactly once.
[1, 2, 2, 3, 1, 4, 3].uniq
# => [1, 2, 3, 4]
Return value: A new array without duplicates. The order of first appearances is preserved, so the first occurrence of each value stays in its original position.
Gotcha, the bang variant of uniq is tricky and behaves differently from most Ruby bang methods:
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 and is a common source of bugs. Never write arr.uniq! || arr; it always returns arr even when uniq! actually did work because the return value of uniq! on a successful deduplication is the array itself, which is truthy.
flatten
flatten collapses nested arrays into a single flat array. It is the right tool when you have a structure of nested lists and need a simple linear sequence.
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. The optional depth argument controls how many levels of nesting to collapse; the default flattens everything.
Gotcha: flatten only flattens arrays. It does not recursively flatten hashes or other nested structures like sets or custom enumerable objects:
[[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 of relying on flatten with a high depth argument.
flatten is best when the structure is only one or two levels deep and the goal is a straightforward list. If the nesting has meaning, flattening too early can hide useful structure. In those cases, it is often better to transform the nested pieces separately and decide later whether they should collapse into one array.
How to chain ruby array methods
One of Ruby’s strengths is that most array methods return arrays, which means you can chain them together cleanly into a single expression. Each method does one job and hands a predictable value to the next method.
# 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]
Chaining is most readable when each step in the chain answers a single question. In the example above, select filters, map transforms, and the result is ready to use. When you add too many steps, break the chain into named intermediate variables so the intent stays clear.
# 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"
The &. at the end of the chain is critical here. find breaks the chain because it returns a single element or nil, so any method called after it needs the safe navigation operator to avoid crashing on a nil result.
# 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, and 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 &.. That chainability is one of the reasons array code in Ruby can stay compact without becoming hard to read.
Mutating vs non-mutating
Most array methods return a new array. The bang (!) variants mutate the original. Here is 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 { } | (none) |
reject | reject { } | (none) |
map | map { } | (none) |
reduce | reduce | (none) |
find | find { } | (none) |
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.
This table is a good reminder that the bang convention is not always uniform. In array code, the biggest risk is assuming every method has a matching in-place version. That is not true for the methods most often used in data pipelines, so it is better to check the return value and assign it intentionally when mutation matters.
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
After filtering and transforming, a common next step is counting occurrences. Ruby array methods make this clean with reduce and a hash that defaults missing keys to zero. The Hash.new(0) trick means you never have to check whether a key exists before incrementing:
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. Without this default, you would need to check whether the key exists before incrementing it.
Safe nested extraction
Find a user and safely dig into nested attributes without crashing on missing data:
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. dig traverses nested hashes and arrays, returning nil for any missing key instead of raising an error.
The broader lesson is that arrays often act as the first step in a wider data flow. You filter the collection, pull out the value you need, then guard the next method call so a missing record does not blow up the whole chain. Small examples like this make that pattern easy to copy into real code.
Frequently asked questions about ruby array methods
What is the difference between map and each?
map returns a new array with the transformed values. each returns the original array and is meant for side effects like printing or logging. If you want a transformed collection, use map; if you want to iterate for its side effects, use each.
When should I use find instead of select?
Use find when you only need the first matching element and can stop scanning once you find it. Use select when you need every element that matches the condition. find is faster for “first match” searches because it stops early.
How do I remove duplicates from a ruby array?
Use uniq to return a new array without duplicates. Use uniq! to modify the original array in place, but remember that uniq! returns nil when no duplicates were found, unlike most bang methods.
See Also
Array#mapreference, transform elements withmapArray#selectreference, filter elements withselectArray#reducereference, accumulate withreduceArray#sortreference, sorting in RubyArray#uniqreference, remove duplicates withuniq