map and select
When you work with a Ruby collection, you almost always want to do one of two things: turn each element into something else, or keep only the elements that matter. Those two jobs have names in Ruby: map transforms, and select filters. They are two of the most-used methods from the Enumerable module, and mastering exactly how each one decides what goes into its result is one of the fastest ways to write clean, idiomatic Ruby instead of manual loops.
Overview: How map and select Work
map and select both come from the Enumerable module, which is mixed into Array, Hash, Range, Set, and any custom class that defines an each method and includes Enumerable. Under the hood, both methods are built on top of each: they walk the collection one element at a time, run your block, and use the block’s return value to build a brand-new array. Neither method is defined separately for Array versus Hash versus Range — the same Enumerable#map and Enumerable#select logic works for all of them, because Ruby’s method lookup finds them once in the shared module rather than duplicating the logic in every class.
The key difference between the two is what they do with the block’s return value:
map (also called collect) uses the block’s return value as the new element itself. Whatever the last expression in your block evaluates to becomes the corresponding entry in the result array. The result always has exactly as many elements as the original collection.
select (also called find_all, or filter since Ruby 2.6) uses the block’s return value only as a truth test. It does not care what the block returns beyond whether that value is truthy or falsy — if the block’s result is truthy, the original element (not the block’s return value) is kept in the result array. If the block’s result is falsy, the element is dropped. The result can be shorter than the original collection, or just as long, but never longer.
This matters because Ruby has a very specific definition of truthy and falsy: only nil and false are falsy. Everything else — including 0, an empty string "", and an empty array [] — is truthy. This trips up people coming from languages like JavaScript, Python, or C, where zero and empty containers are often falsy. In Ruby, numbers.select { |n| n } keeps every number, including zero, because every integer is a truthy object.
map and select never mutate by default
Both methods return a completely new array and leave the receiver untouched. Ruby signals "this method mutates the receiver" with a trailing !, so the mutating counterparts are map! and select!. map! replaces each element of the array in place. select! removes elements that don’t match — and, like sort!, it returns nil if calling it didn’t actually remove anything, which is a common source of bugs when you chain off its return value.
Syntax
The general forms, with the block written as a one-line { ... } or a multi-line do ... end:
collection.map { |item| transformed_item }
collection.map do |item|
transformed_item
end
collection.select { |item| boolean_condition }
collection.select do |item|
boolean_condition
end
collection.map(&:method_name) # Symbol#to_proc shorthand
- collection — any object that includes
Enumerable: anArray,Hash,Range,Set, and so on. - item — the block parameter, bound to the current element on each iteration (for a
Hash, this is a[key, value]pair unless you destructure it as|key, value|). - transformed_item — for
map, the last expression evaluated in the block; it becomes the new element in the result array. - boolean_condition — for
select, the last expression evaluated in the block; only its truthiness matters, not its value. &:method_name— a shorthand block that callsmethod_nameon each element with no arguments, usingSymbol#to_proc.list.map(&:to_s)is equivalent tolist.map { |x| x.to_s }.
| Method | Aliases | Result length | What’s collected |
|---|---|---|---|
map |
collect |
same as original | the block’s return value |
select |
find_all, filter |
0 to original length | the original element (if block is truthy) |
reject |
— | 0 to original length | the original element (if block is falsy) |
map! |
collect! |
same, mutates receiver | the block’s return value |
select! |
filter! |
mutates receiver, returns nil if unchanged |
the original element |
Examples
Example 1: Transforming numbers with map
numbers = [1, 2, 3, 4, 5]
squares = numbers.map { |n| n * n }
puts squares.inspect
puts numbers.inspect
Output:
[1, 4, 9, 16, 25]
[1, 2, 3, 4, 5]
The block runs once per element, and its return value (n * n) becomes the corresponding entry in the new array. Notice that numbers itself is completely unchanged afterward — map built a fresh array rather than modifying the original.
Example 2: Filtering with select
numbers = (1..10).to_a
evens = numbers.select { |n| n.even? }
puts evens.inspect
Output:
[2, 4, 6, 8, 10]
Here the block returns a boolean from even?, but select only uses that to decide inclusion — the values placed into evens are the original integers, not true/false. Odd numbers, where the block evaluates to false, are dropped.
Example 3: Chaining select and map on structured data
Person = Struct.new(:name, :active)
people = [
Person.new("Ava", true),
Person.new("Ben", false),
Person.new("Cy", true)
]
active_names = people.select { |p| p.active }.map(&:name)
puts active_names.inspect
Output:
["Ava", "Cy"]
This is the realistic pattern: first narrow the collection down with select (keep only active people), then transform what’s left with map (pull out just the name). The second step uses the &:name shorthand instead of a full block since it’s just calling one method with no arguments on each element.
How map and select Work Step by Step
- Ruby calls the collection’s underlying
eachmethod internally. - For every element, the block is invoked with that element bound to the block parameter.
- For
map: the value of the block’s last evaluated expression is appended to a new accumulator array. - For
select: the value of the block’s last evaluated expression is checked for truthiness. If it is anything other thannilorfalse, the original element (not the block’s return value) is appended to the accumulator array. Otherwise nothing is appended. - Once every element has been visited, the accumulator array is returned. The receiver itself was never touched.
Common Mistakes
Mistake 1: Using select when you meant map
Because select only cares about truthiness, it’s easy to accidentally write a block that always evaluates truthy and get the whole collection back unfiltered and untransformed:
words = ["apple", "banana", "kiwi"]
lengths = words.select { |w| w.length }
puts lengths.inspect
Output:
["apple", "banana", "kiwi"]
This looks like it should produce a list of lengths, but select discards the block’s return value except to test it for truthiness. Every string’s length (5, 6, and 4) is a nonzero integer, and in Ruby every integer, including 0, is truthy — only nil and false are falsy. So all three original strings pass the truth test and come back unchanged. The fix is to use map, which uses the block’s return value as the new element instead of just testing it:
words = ["apple", "banana", "kiwi"]
lengths = words.map { |w| w.length }
puts lengths.inspect
Output:
[5, 6, 4]
Mistake 2: Mutating shared objects inside map
map builds a new array, but if the elements are mutable objects (like strings) and your block calls a bang method on them, you mutate the very same objects the original collection is still holding onto — and bang methods often return nil when there was nothing to change, silently poisoning your new array too:
names = ["Alice", "BOB"]
shouted = names.map { |n| n.upcase! }
puts shouted.inspect
puts names.inspect
Output:
["ALICE", nil]
["ALICE", "BOB"]
Two things go wrong here. First, "Alice".upcase! mutates the string in place and returns it, but "BOB".upcase! has nothing to change (it’s already all uppercase) so it returns nil — and that nil, not "BOB", is what lands in shouted. Second, because map only creates a new array, not new copies of the elements inside it, mutating n with upcase! also mutates the original names array’s first string, since both arrays are pointing at the very same String object. The fix is to use the non-mutating upcase, which always returns a new string and never touches the original:
names = ["Alice", "BOB"]
shouted = names.map { |n| n.upcase }
puts shouted.inspect
puts names.inspect
Output:
["ALICE", "BOB"]
["Alice", "BOB"]
Best Practices
- Reach for
mapwhen you need a same-length collection of transformed values, andselect(or its complementreject) when you need a subset of the original elements. - Don’t use
mappurely for its side effects when you’re discarding the return value — if you’re not using the resulting array, useeachinstead so the intent is clear. - Prefer non-mutating methods (
upcase,sort,reverse) inside amaporselectblock unless you specifically intend to mutate the original elements. - Remember that
select!and other bang Enumerable methods returnnilwhen nothing changed — never chain another method directly off their return value. - Use the
&:method_nameshorthand for simple one-method blocks likelist.map(&:to_s)to keep code concise. - Chain
selectandmaptogether (or usefilter_mapon Ruby 2.7+) when you need to filter and transform in the same pipeline, rather than writing a manual loop. - Remember Ruby’s truthiness rules when writing a
selectcondition:0,"", and[]are all truthy, so a condition like{ |x| x }keeps everything exceptnilandfalse.
Practice Exercises
- Given
words = ["ruby", "go", "python", "c"], useselectto build an array of only the words with more than two characters. Expected output:["ruby", "python"]. - Given
prices = [19.99, 5.50, 42.00, 3.25], usemapto build a new array where each price has a 10% discount applied (round to 2 decimal places with.round(2)). - Given
numbers = (1..20).to_a, chainselectandmapin one line to first keep only multiples of 3, then square each one. Try writing it two ways: withselectthenmap, and with a singlefilter_mapcall.
Summary
map(aliascollect) transforms every element, using the block’s return value as the new element; the result is always the same length as the original.select(aliasesfind_all,filter) filters elements, using the block’s return value only to test truthiness; the original elements — not the block’s return value — are what get kept.- In Ruby, only
nilandfalseare falsy;0,"", and[]are all truthy, which directly affects howselectconditions behave. - Both methods return new arrays and never mutate the receiver; use
map!andselect!for in-place mutation, and watch out forselect!returningnilwhen nothing was removed. - Mutating objects with bang methods inside a
mapblock can silently mutate the original collection too, since both arrays reference the same underlying objects. selectandmapchain naturally to build filter-then-transform pipelines, which is idiomatic Ruby in place of manual index-based loops.
