rubyguides

Enumerable#map

What does map do?

map transforms every element in a collection by passing it through a block, and returns a new array containing the results. The original collection is left unchanged.

Think of it as: “take each item, do something to it, and collect the results.”

Unlike each, which iterates purely for side effects and returns the original collection, map collects and returns what the block produces for each element.

Basic Usage

With a block

[1, 2, 3].map { |n| n * 2 }   # => [2, 4, 6]
["hello", "world"].map(&:upcase)  # => ["HELLO", "WORLD"]

Both calls produce a fresh array where each element has been transformed by the block. The first example multiplies each number by two; the second calls upcase on each string via the &:method shorthand. The original arrays are never touched — map is a non-mutating method, so you can safely pass the same collection through multiple transformations without worrying about side effects.

Without a block

When called without a block, map returns an Enumerator:

[1, 2, 3].map   # => #<Enumerator: [1, 2, 3]:map>

Without a block, map returns an Enumerator — a lazy object that remembers the collection and the method but does not execute until you consume it. This deferred execution is what lets you chain map with other methods like with_index or with_object. The enumerator acts as a placeholder that says “I know how to map, but I will wait until you tell me what to do next.”

This allows chaining with other Enumerable methods:

[1, 2, 3, 4, 5].map.with_index { |n, i| [i, n] }
# => [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5]]

The with_index modifier adds a zero-based index to each element, converting what would be a simple value transformation into a position-aware operation. Each result is a two-element array of [index, value], which is a common pattern for building lookup tables or associating data with its original position. You can use any starting index by passing an offset to with_index(1).

Practical Examples

Extracting attributes

users = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Carol", age: 35 }
]

users.map { |u| u[:name] }   # => ["Alice", "Bob", "Carol"]
users.map { |u| u[:age] }   # => [30, 25, 35]

Extracting a single attribute from an array of hashes is one of the most common uses of map. Each hash is yielded to the block, and the block returns the value for the key you specify. The result is a flat array of just that attribute — names become ["Alice", "Bob", "Carol"] and ages become [30, 25, 35]. This pattern is especially useful when preparing data for display or feeding it into another method that expects a simple array.

Converting types

["1", "2", "3"].map(&:to_i)         # => [1, 2, 3]
[1.5, 2.7, 3.1].map(&:to_i)        # => [1, 2, 3]
[100, 200, 300].map { |n| n.to_s }  # => ["100", "200", "300"]

Type conversion with map is the Ruby equivalent of casting every element in a collection. The &:to_i shorthand calls Integer#to_i on each string, and Float#to_i on each float (which truncates the decimal portion). Note that map(&:to_i) on floats produces [1, 2, 3], not [2, 3, 3] — the truncation happens per-element, not via rounding.

Transforming hashes

When called on a hash, map yields key-value pairs:

ages = { alice: 30, bob: 25, carol: 35 }

# Returns an array of arrays
ages.map { |name, age| [name, age * 2] }.to_h
# => { alice: 60, bob: 50, carol: 70 }

# Extract just the keys
ages.map { |name, _| name.to_s }  # => ["alice", "bob", "carol"]

When map iterates over a hash, each key-value pair is destructured into two block parameters. The block must return two-element arrays for to_h to reconstruct a hash from the result. If you only need the keys or values, you can ignore the unused parameter with an underscore. The result is always a flat array, even when starting from a hash — use to_h at the end if you need the hash shape back.

Nested mapping

matrix = [[1, 2], [3, 4], [5, 6]]

matrix.map { |row| row.map { |n| n * 10 } }
# => [[10, 20], [30, 40], [50, 60]]

Nested map calls preserve the structure of the original data while transforming the innermost values. The outer map walks over each row, and the inner map multiplies each number within that row by ten. The two-dimensional shape of the result matches the input exactly. This is a higher-order pattern — each call to map operates at a different level of nesting, and together they produce a transformed copy without flattening or restructuring.

Using with_index

fruits = ["apple", "banana", "cherry"]

fruits.map.with_index { |fruit, i| "#{i + 1}. #{fruit}" }
# => ["1. apple", "2. banana", "3. cherry"]

The block receives both the fruit name and its zero-based index. Adding 1 to the index produces a human-readable numbered list starting at 1 instead of 0. String interpolation builds each label in one expression, and map collects the results into a single array. This technique is frequently used when generating display labels, menu items, or any output where position matters alongside the data.

Symbol to proc shorthand

[1, 2, 3, 4, 5].map(&:succ)    # => [2, 3, 4, 5, 6]
["hello", "world"].map(&:length)  # => [5, 5]

The &:succ syntax is Ruby’s symbol-to-proc shorthand: it converts the symbol :succ into a block that calls succ on each element. Integer#succ returns the successor (the next integer), so [1, 2, 3, 4, 5].map(&:succ) gives [2, 3, 4, 5, 6]. This pattern is available for any method that takes no arguments and returns a value — just ensure the method name makes sense in the mapping context.

map vs each: the key difference

The most common mistake is confusing map with each. They look similar but behave very differently:

MethodReturnsUse case
eachOriginal collection (unchanged)Iterating for side effects
mapNew array of block resultsTransforming elements
numbers = [1, 2, 3]

result = numbers.each { |n| n * 2 }
result  # => [1, 2, 3] - original, unchanged!

result = numbers.map { |n| n * 2 }
result  # => [2, 4, 6] - new array with transformed values

The numbers inside the each block get multiplied, but the results are discarded — each returns the original array, not the values the block computed. map, on the other hand, captures every return value from the block and assembles them into a new array. This is the distinction that trips up beginners most often: both methods visit every element, but only map cares about what the block returns.

When to use each

Use each when you want to perform side effects — printing, writing to a file, updating external state:

[1, 2, 3].each { |n| puts n * 2 }
# Output:
# 2
# 4
# 6
# => [1, 2, 3]

This example makes the side-effect nature of each explicit: the printed output appears on screen, but the return value is the unmodified array [1, 2, 3]. The block body does real work (printing) yet contributes nothing to the return. When you see each in code, expect side effects like logging, network calls, or state mutation — not a collection of computed values.

When to use map

Use map when you want to derive a new collection from an existing one:

names = ["alice", "bob", "carol"]
names.map(&:capitalize)   # => ["Alice", "Bob", "Carol"]

The transformation here is straightforward: capitalize turns each lowercase name into its title-cased form. The block result — the capitalized string — is collected into the new array. This kind of one-to-one transformation is where map shines: every input produces exactly one output, and the shape of the collection stays the same while the values change.

collect: The Alias

collect is an exact synonym for map in Ruby. They are identical — same implementation, same return type, same behaviour. Use whichever reads more naturally in your context.

[1, 2, 3].map    { |n| n * 2 }   # => [2, 4, 6]
[1, 2, 3].collect { |n| n * 2 }  # => [2, 4, 6]

map is more common in Ruby code because it reads as “transform each element into something.” collect is the historical name from Smalltalk, Ruby’s predecessor in this design. The C implementation in MRI dispatches both method names to the same function, so there is zero runtime difference between them. Choose whichever name aligns with how your team thinks about the operation — map for transformation pipelines and collect for gathering results.

# Both produce identical results:
numbers.map(&:to_s)
numbers.collect(&:to_s)

The two calls produce the same output because they share identical C-level dispatch. Whether you write map(&:to_s) or collect(&:to_s), Ruby converts each number to its string representation and assembles the results into a new array. The choice is stylistic — some teams standardise on map for method chaining and reserve collect for standalone calls, but the interpreter does not distinguish between them.

chaining with other enumerable methods

map returns an array, so you can chain it with other Enumerable methods:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  .select(&:even?)
  .map { |n| n * n }
# => [4, 16, 36, 64, 100]

# Filter out nils with compact
["1", "two", "3", nil, "four"].map { |s| s.to_i if s.is_a?(String) }.compact
# => [1, 3, 4]

The first chain filters the numbers down to evens with select, then squares them with map. The second chain demonstrates a subtle point: when map encounters a nil in the input, the conditional if s.is_a?(String) returns nil for that element, which compact later strips out. This map-then-compact pattern is common enough that Ruby 2.7 introduced filter_map to do both steps in a single pass without producing intermediate nils.

Common chains

# Select then map (or map then select — order matters!)
[1, 2, 3, 4, 5].select(&:odd?).map { |n| n * 10 }  # => [10, 30, 50]
[1, 2, 3, 4, 5].map { |n| n * 10 }.select(&:even?)  # => [20, 40]

# Map then reduce
[1, 2, 3, 4].map { |n| n * 2 }.reduce(:+)  # => 20

# Map then flatten
[[1, 2], [3, 4]].map { |arr| arr.map { |n| n + 1 } }
# => [[2, 3], [4, 5]]

These chains show how map fits into larger data pipelines. The order of select and map changes the output because each step operates on the result of the previous one. Running select(&:odd?) first keeps [1, 3, 5], which then get multiplied. Running map first multiplies everything to [10, 20, 30, 40, 50], and then select(&:even?) keeps only [20, 40]. The chain is read left to right, top to bottom — each method call narrows or reshapes the data flowing through.

Using flat_map

flat_map combines map and flatten in one pass:

[[1, 2], [3, 4]].map { |arr| arr.map { |n| n * 2 } }
# => [[2, 4], [6, 8]] - array of arrays

[[1, 2], [3, 4]].flat_map { |arr| arr.map { |n| n * 2 } }
# => [2, 4, 6, 8] - flattened

flat_map is equivalent to map followed by flatten(1), but it does both in a single pass and avoids creating the intermediate nested array. The outer block still calls map on each inner array, but flat_map concatenates those inner results into one flat output. For deeply nested data, flat_map with a recursive strategy can collapse multiple levels at once, though the classic use case is the single-level flatten shown here.

Performance Considerations

map has O(n) time complexity — it visits each element exactly once. It always processes the full collection and creates a new array of the same size.

Memory usage

map creates and returns a new array. For very large collections, this can be significant. If you only need to iterate once (e.g., printing), use each.

# Memory: creates a full new array
squares = (1..1_000_000).map { |n| n**2 }  # lots of memory

# If you just need to print squares:
(1..1_000_000).each { |n| puts n**2 }       # no extra array created

The memory contrast is stark: map allocates an array of one million integers, while each streams values to puts without storing any of them. For a one-time iteration where you do not need the transformed collection afterward, each is the memory-conscious choice. If you need the transformed values later, the allocation is the price of retaining them.

Lazy evaluation with lazy.map

For large or infinite collections, use lazy to defer evaluation:

# Eager: processes all immediately
(1..10).map { |n| n * 2 }.first(3)  # => [2, 4, 6]
# (actually computes all, then takes first 3)

# Lazy: processes only what's needed
(1..Float::INFINITY).lazy.map { |n| n * 2 }.first(3)
# => [2, 4, 6]
# (stops after producing 3 results)

The eager version builds the full [2, 4, 6, 8, 10, 12, 14, 16, 18, 20] array before first(3) selects the first three elements. The lazy version calls the block only three times — once for each element first requests — and never touches elements 4 through 10. On an infinite range, eager map would run forever, but lazy map stops as soon as the consumer is satisfied. This makes lazy essential for working with unbounded streams or minimizing work on large datasets.

Hash Considerations

When map is called on a hash, it yields [key, value] pairs:

scores = { alice: 100, bob: 85, carol: 92 }

# By default, returns an array of two-element arrays
scores.map { |name, score| "#{name}: #{score}" }
# => ["alice: 100", "bob: 85", "carol: 92"]

# To get a hash back, convert with `to_h`
scores.map { |name, score| [name, score + 10] }.to_h
# => { alice: 110, bob: 95, carol: 102 }

The first call produces an array of strings because the block returns a single interpolated string for each key-value pair. The second call returns an array of two-element arrays — [name, score + 10] — which to_h then reassembles into a hash. This is the standard pattern for transforming hash values while preserving the key structure: map to an array of pairs, then call to_h.

Summary

  • map transforms each element and returns a new array
  • collect is an exact alias — use whichever reads better
  • each returns the original; map returns transformed results
  • Without a block, map returns an Enumerator (lazy by default when chained)
  • map always returns an array, even on hashes (use to_h if you need a hash back)

See Also