map and select

Published by Programming Line in Ruby. Last updated .

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: an Array, 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 calls method_name on each element with no arguments, using Symbol#to_proc. list.map(&:to_s) is equivalent to list.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

  1. Ruby calls the collection’s underlying each method internally.
  2. For every element, the block is invoked with that element bound to the block parameter.
  3. For map: the value of the block’s last evaluated expression is appended to a new accumulator array.
  4. For select: the value of the block’s last evaluated expression is checked for truthiness. If it is anything other than nil or false, the original element (not the block’s return value) is appended to the accumulator array. Otherwise nothing is appended.
  5. 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 map when you need a same-length collection of transformed values, and select (or its complement reject) when you need a subset of the original elements.
  • Don’t use map purely for its side effects when you’re discarding the return value — if you’re not using the resulting array, use each instead so the intent is clear.
  • Prefer non-mutating methods (upcase, sort, reverse) inside a map or select block unless you specifically intend to mutate the original elements.
  • Remember that select! and other bang Enumerable methods return nil when nothing changed — never chain another method directly off their return value.
  • Use the &:method_name shorthand for simple one-method blocks like list.map(&:to_s) to keep code concise.
  • Chain select and map together (or use filter_map on 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 select condition: 0, "", and [] are all truthy, so a condition like { |x| x } keeps everything except nil and false.

Practice Exercises

  • Given words = ["ruby", "go", "python", "c"], use select to 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], use map to 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, chain select and map in one line to first keep only multiples of 3, then square each one. Try writing it two ways: with select then map, and with a single filter_map call.

Summary

  • map (alias collect) transforms every element, using the block’s return value as the new element; the result is always the same length as the original.
  • select (aliases find_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 nil and false are falsy; 0, "", and [] are all truthy, which directly affects how select conditions behave.
  • Both methods return new arrays and never mutate the receiver; use map! and select! for in-place mutation, and watch out for select! returning nil when nothing was removed.
  • Mutating objects with bang methods inside a map block can silently mutate the original collection too, since both arrays reference the same underlying objects.
  • select and map chain naturally to build filter-then-transform pipelines, which is idiomatic Ruby in place of manual index-based loops.