Overview
Map, filter, and reduce represent three fundamental higher-order functions that operate on collections. These functions accept other functions as arguments and apply them systematically to collection elements. The pattern originated in functional programming languages like Lisp and has become standard across modern programming languages.
Map transforms each element in a collection by applying a function, producing a new collection of the same size. Filter selects elements that satisfy a predicate condition, producing a potentially smaller collection. Reduce combines all elements into a single accumulated value through repeated application of a binary function.
These operations provide declarative alternatives to imperative iteration. Instead of writing explicit loops with mutable state, developers express transformations as function compositions. This approach reduces boilerplate code, minimizes state management errors, and makes data transformations more readable.
# Traditional imperative approach
numbers = [1, 2, 3, 4, 5]
doubled = []
numbers.each { |n| doubled << n * 2 }
# => [2, 4, 6, 8, 10]
# Functional approach with map
numbers.map { |n| n * 2 }
# => [2, 4, 6, 8, 10]
The functional approach expresses intent directly: transform each number by doubling it. The imperative version requires tracking a separate array, explicit iteration, and manual insertion. Map, filter, and reduce eliminate this ceremony while making transformations composable and testable.
Key Principles
Map, filter, and reduce operate through function application on collection elements. Each function represents a distinct transformation pattern with specific mathematical properties and guarantees.
Map applies a unary function to each element independently, producing a new collection with identical structure. The mapping function receives one element and returns one transformed element. Map preserves collection size and iteration order. The operation distributes over concatenation: mapping over combined collections equals combining mapped collections.
# Map transforms each element independently
['apple', 'banana', 'cherry'].map(&:upcase)
# => ['APPLE', 'BANANA', 'CHERRY']
# Transformation function determines output type
[1, 2, 3].map { |n| n.to_s }
# => ['1', '2', '3']
Map operations compose naturally. Applying multiple maps sequentially equals applying their composed function once. This property enables optimization in lazy evaluation contexts where multiple transformations can fuse into a single pass.
Filter evaluates a predicate function against each element, retaining only elements where the predicate returns true. The predicate must return a boolean value. Filter produces a collection of equal or smaller size while maintaining element order. Elements that satisfy the predicate appear in the result in their original sequence.
# Filter selects elements matching a condition
[1, 2, 3, 4, 5, 6].select { |n| n.even? }
# => [2, 4, 6]
# Predicate determines inclusion
words = ['cat', 'elephant', 'dog', 'butterfly']
words.select { |w| w.length > 3 }
# => ['elephant', 'butterfly']
Filter operations exhibit idempotence when using the same predicate: filtering an already-filtered collection produces the same result. Multiple filter operations compose conjunctively—an element must satisfy all predicates to appear in the final result.
Reduce combines elements through repeated application of a binary function with an accumulator. The binary function receives the current accumulator value and the next element, returning the new accumulator. Reduce consumes the entire collection to produce a single value. The operation requires an initial accumulator value, either explicitly provided or derived from the first element.
# Reduce accumulates values
[1, 2, 3, 4, 5].reduce(0) { |sum, n| sum + n }
# => 15
# Binary function defines combination logic
['hello', 'world', 'ruby'].reduce('') { |acc, word| acc + word + ' ' }
# => 'hello world ruby '
Reduce generalizes many collection operations. Counting, summing, finding minimums and maximums, and building complex data structures all represent specific reduce applications. The accumulator can be any type: numbers, strings, arrays, hashes, or custom objects.
The order of evaluation matters for reduce. Left reduction processes elements from start to end, while right reduction processes from end to start. For associative operations like addition, direction does not affect the result. For non-associative operations, left and right reduction produce different outcomes.
These three operations form a complete basis for collection transformations. Complex data processing pipelines decompose into sequences of maps, filters, and reduces. The separation of concerns—transformation, selection, and aggregation—creates clear, maintainable code.
Ruby Implementation
Ruby provides native implementations of map, filter, and reduce through the Enumerable module. Any class including Enumerable gains access to these methods. Arrays, hashes, ranges, and custom collection classes all support these operations.
Map Operations in Ruby appear as map and collect (synonyms). Both return a new array containing transformed elements. Ruby also provides map! for in-place transformation of arrays.
# Basic map usage
[1, 2, 3].map { |n| n * n }
# => [1, 4, 9]
# Symbol-to-proc syntax for method calls
['ruby', 'python', 'javascript'].map(&:upcase)
# => ['RUBY', 'PYTHON', 'JAVASCRIPT']
# Map with index access
['a', 'b', 'c'].map.with_index { |char, i| "#{i}: #{char}" }
# => ['0: a', '1: b', '2: c']
# Chaining transformations
(1..5).map { |n| n * 2 }.map { |n| n + 1 }
# => [3, 5, 7, 9, 11]
The map method works with any enumerable. Hashes produce arrays of transformed key-value pairs. Using to_h converts the result back to a hash.
# Mapping over hash entries
{ a: 1, b: 2, c: 3 }.map { |key, value| [key, value * 2] }.to_h
# => { a: 2, b: 4, c: 6 }
# Transforming only values
{ a: 1, b: 2 }.transform_values { |v| v * 2 }
# => { a: 2, b: 4 }
Filter Operations appear as select and filter (synonyms), plus reject for inverse filtering. These methods return elements satisfying or violating a predicate.
# Select elements matching condition
(1..10).select { |n| n % 3 == 0 }
# => [3, 6, 9]
# Reject filters inversely
[1, 2, 3, 4, 5].reject { |n| n.even? }
# => [1, 3, 5]
# Filter with multiple conditions
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers.select { |n| n.even? && n > 5 }
# => [6, 8, 10]
# Filtering hashes
{ a: 1, b: 2, c: 3, d: 4 }.select { |k, v| v.even? }
# => { b: 2, d: 4 }
Ruby provides specialized filter methods: grep for pattern matching, grep_v for inverse pattern matching, and find_all as a select synonym.
# Pattern-based filtering
['apple', 'banana', 'apricot', 'cherry'].grep(/^a/)
# => ['apple', 'apricot']
# Numeric range filtering
[1, 5, 3, 8, 2, 9].grep(3..7)
# => [5, 3]
Reduce Operations in Ruby use reduce or inject (synonyms). Both accept an initial value and a block defining the accumulation logic. Without an initial value, the first element becomes the accumulator.
# Reduce with initial value
[1, 2, 3, 4].reduce(0) { |sum, n| sum + n }
# => 10
# Reduce without initial value (uses first element)
[1, 2, 3, 4].reduce { |product, n| product * n }
# => 24
# Symbol-based reduce for common operations
[1, 2, 3, 4].reduce(:+)
# => 10
# Building complex structures
words = ['cat', 'dog', 'bird']
words.reduce({}) { |hash, word| hash.merge(word => word.length) }
# => { 'cat' => 3, 'dog' => 3, 'bird' => 4 }
Ruby's reduce handles various accumulator types. The accumulator can be a number, string, array, hash, or custom object. The block's return value becomes the next accumulator value.
# Reducing to array
[1, 2, 3, 4, 5].reduce([]) { |evens, n| n.even? ? evens << n : evens }
# => [2, 4]
# Reducing to find maximum
[3, 7, 2, 9, 4].reduce { |max, n| n > max ? n : max }
# => 9
# Grouping with reduce
numbers = [1, 2, 3, 4, 5, 6]
numbers.reduce({ even: [], odd: [] }) do |groups, n|
n.even? ? groups[:even] << n : groups[:odd] << n
groups
end
# => { even: [2, 4, 6], odd: [1, 3, 5] }
Ruby provides reduce_right through the reverse_each combination for right-to-left reduction, though this is rarely needed due to most operations being left-associative.
Practical Examples
Data Transformation Pipeline: Processing user data requires multiple transformation steps. Map, filter, and reduce combine to create clean, readable pipelines.
users = [
{ name: 'Alice', age: 30, active: true, purchases: 5 },
{ name: 'Bob', age: 25, active: false, purchases: 2 },
{ name: 'Carol', age: 35, active: true, purchases: 8 },
{ name: 'Dave', age: 28, active: true, purchases: 12 }
]
# Extract names of active users who made more than 5 purchases
power_users = users
.select { |u| u[:active] }
.select { |u| u[:purchases] > 5 }
.map { |u| u[:name] }
# => ['Carol', 'Dave']
# Calculate total purchases from active users
active_purchases = users
.select { |u| u[:active] }
.map { |u| u[:purchases] }
.reduce(0, :+)
# => 25
The pipeline reads naturally: filter active users, filter by purchase count, extract names. Each step operates independently, making the logic clear and testable.
Text Analysis: Analyzing word frequency demonstrates reduce building complex data structures.
text = "the quick brown fox jumps over the lazy dog the fox"
words = text.downcase.split
# Count word frequencies
frequencies = words.reduce(Hash.new(0)) do |counts, word|
counts[word] += 1
counts
end
# => { 'the' => 3, 'quick' => 1, 'brown' => 1, 'fox' => 2, ... }
# Find most common words
top_words = frequencies
.select { |word, count| count > 1 }
.map { |word, count| [word, count] }
.sort_by { |word, count| -count }
.take(3)
.map { |word, count| word }
# => ['the', 'fox']
The hash accumulator tracks counts. Each word increments its counter. Filter removes single-occurrence words, map extracts pairs, sort orders by frequency, and final map extracts words.
Matrix Operations: Applying transformations to two-dimensional data structures.
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Double all values
doubled = matrix.map { |row| row.map { |n| n * 2 } }
# => [[2, 4, 6], [8, 10, 12], [14, 16, 18]]
# Filter rows containing even numbers
rows_with_evens = matrix.select { |row| row.any?(&:even?) }
# => [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Sum all elements
total = matrix.reduce(0) { |sum, row| sum + row.reduce(:+) }
# => 45
# Flatten and process
flattened_sum = matrix
.flat_map { |row| row }
.select { |n| n > 3 }
.reduce(:+)
# => 39
Nested maps handle two-dimensional transformations. The outer map processes rows, inner maps process elements. Combining flat_map with other operations simplifies nested structure processing.
Financial Calculations: Processing transaction data for reporting.
transactions = [
{ date: '2024-01', amount: 100, type: 'sale' },
{ date: '2024-01', amount: 50, type: 'refund' },
{ date: '2024-02', amount: 200, type: 'sale' },
{ date: '2024-02', amount: 150, type: 'sale' },
{ date: '2024-02', amount: 25, type: 'refund' }
]
# Calculate net revenue by month
monthly_revenue = transactions.reduce(Hash.new(0)) do |totals, t|
adjustment = t[:type] == 'sale' ? t[:amount] : -t[:amount]
totals[t[:date]] += adjustment
totals
end
# => { '2024-01' => 50, '2024-02' => 325 }
# Get sales over 100
large_sales = transactions
.select { |t| t[:type] == 'sale' }
.select { |t| t[:amount] > 100 }
.map { |t| t[:amount] }
# => [200, 150]
# Convert amounts to different currency
exchange_rate = 1.2
converted = transactions.map do |t|
t.merge(amount: t[:amount] * exchange_rate)
end
Reduce builds the monthly totals hash. Each transaction adjusts its month's total based on type. Filter chains isolate specific transaction categories. Map transforms without modifying original data.
Common Patterns
Chain Composition: Multiple map, filter, and reduce operations combine to form data processing pipelines. Each operation transforms the collection for the next operation.
# Processing order matters
result = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.select { |n| n > 3 } # Filter first: [4, 5, 6, 7, 8, 9, 10]
.map { |n| n * 2 } # Then transform: [8, 10, 12, 14, 16, 18, 20]
.reduce(:+) # Finally aggregate: 98
# Compare with different ordering
alternative = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.map { |n| n * 2 } # Transform first: [2, 4, 6, ... 20]
.select { |n| n > 6 } # Then filter: [8, 10, 12, 14, 16, 18, 20]
.reduce(:+) # Same result: 98
Filter-then-map generally performs better than map-then-filter. Filtering reduces the collection size before expensive transformations. However, the specific order depends on predicate and transformation costs.
Partition Pattern: Separating collections into groups based on criteria uses reduce to build categorized structures.
# Partition by condition
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
partitioned = numbers.reduce({ even: [], odd: [] }) do |groups, n|
n.even? ? groups[:even] << n : groups[:odd] << n
groups
end
# => { even: [2, 4, 6, 8, 10], odd: [1, 3, 5, 7, 9] }
# Ruby provides partition method as shorthand
even, odd = numbers.partition(&:even?)
# even => [2, 4, 6, 8, 10]
# odd => [1, 3, 5, 7, 9]
# Multi-way partition requires reduce
numbers.reduce({ small: [], medium: [], large: [] }) do |groups, n|
category = n < 4 ? :small : n < 8 ? :medium : :large
groups[category] << n
groups
end
The reduce-based partition builds a hash with array values. Each element appends to the appropriate category. The accumulator must be returned from the block to propagate through iterations.
Index Building: Creating lookup structures from collections uses reduce to build hashes or other indexed data structures.
users = [
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Carol', email: 'carol@example.com' }
]
# Build ID-to-user index
by_id = users.reduce({}) { |index, user| index.merge(user[:id] => user) }
# => { 1 => {...}, 2 => {...}, 3 => {...} }
# Build email-to-name index
by_email = users.reduce({}) do |index, user|
index[user[:email]] = user[:name]
index
end
# => { 'alice@example.com' => 'Alice', ... }
# Build multi-value index (users by first letter)
by_initial = users.reduce(Hash.new { |h, k| h[k] = [] }) do |index, user|
initial = user[:name][0]
index[initial] << user
index
end
Hash accumulators create various index structures. The pattern merges each element into the index under computed keys. Default hash values handle missing keys when building multi-value indexes.
Flat Mapping: Transforming collections where each element maps to multiple output elements combines map with flattening.
# Each user has multiple roles
users = [
{ name: 'Alice', roles: ['admin', 'user'] },
{ name: 'Bob', roles: ['user'] },
{ name: 'Carol', roles: ['admin', 'moderator', 'user'] }
]
# Extract all roles
all_roles = users.flat_map { |u| u[:roles] }
# => ['admin', 'user', 'user', 'admin', 'moderator', 'user']
# Get unique roles
unique_roles = users.flat_map { |u| u[:roles] }.uniq
# => ['admin', 'user', 'moderator']
# Equivalent to map followed by flatten
all_roles_equivalent = users.map { |u| u[:roles] }.flatten
Flat map applies a transformation returning arrays, then concatenates results. This pattern handles one-to-many relationships. The operation is equivalent to map followed by flatten(1).
Presence Checking: Validating that all or any elements satisfy a condition uses short-circuiting predicates built on reduce concepts.
numbers = [2, 4, 6, 8, 10]
# Check if all elements satisfy condition
all_even = numbers.all? { |n| n.even? }
# => true
# Check if any element satisfies condition
has_large = numbers.any? { |n| n > 5 }
# => true
# Manual implementation using reduce
all_even_manual = numbers.reduce(true) { |result, n| result && n.even? }
has_large_manual = numbers.reduce(false) { |result, n| result || n > 5 }
While Ruby provides all? and any? methods, understanding their reduce-based implementation clarifies their behavior. These methods short-circuit: all? stops at the first false, any? stops at the first true.
Performance Considerations
Map, filter, and reduce operations iterate through collections. Performance depends on collection size, operation complexity, and evaluation strategy. Understanding these factors enables optimization for large datasets.
Eager vs Lazy Evaluation: Standard Ruby enumerable methods evaluate immediately. Each operation produces a complete intermediate collection. Chaining multiple operations creates multiple intermediate arrays.
# Eager evaluation creates intermediate arrays
result = (1..1_000_000)
.map { |n| n * 2 } # Creates 1M element array
.select { |n| n > 100 } # Creates another array
.take(10) # Creates final 10 element array
# Lazy evaluation defers computation
lazy_result = (1..1_000_000).lazy
.map { |n| n * 2 }
.select { |n| n > 100 }
.take(10)
.force # Only computes 51 elements
Lazy evaluation chains operations without intermediate arrays. Elements flow through the pipeline one at a time. This approach reduces memory usage and can improve performance when early termination is possible. Use lazy for large collections with filtering or limited result sizes.
Operation Ordering: The sequence of operations affects performance. Filter before map reduces the transformation workload. Select cheap operations before expensive ones.
# Inefficient: map before filter
slow = data
.map { |item| expensive_transformation(item) }
.select { |item| cheap_predicate(item) }
# Efficient: filter before map
fast = data
.select { |item| cheap_predicate(item) }
.map { |item| expensive_transformation(item) }
Filtering first processes fewer elements through expensive transformations. Place filter operations as early as possible in pipelines. Order multiple filters by selectivity: most restrictive predicates first.
Reduce Accumulator Complexity: Reduce performance depends on accumulator operations. String concatenation and array append have different characteristics.
# Inefficient: string concatenation in reduce
slow_join = words.reduce('') { |str, word| str + word + ' ' }
# Each concatenation creates new string: O(n²)
# Efficient: array accumulation then join
fast_join = words.reduce([]) { |arr, word| arr << word }.join(' ')
# Array append is O(1), final join is O(n)
# Even better: use built-in join
best_join = words.join(' ')
Accumulator mutations should have constant time complexity. String concatenation in Ruby creates new strings, leading to quadratic behavior. Array append has amortized constant time. For string building, accumulate in an array then join.
Block Complexity: The transformation, predicate, or accumulation function determines per-element cost. Simple arithmetic operations differ from complex computations or I/O operations.
# Fast: simple arithmetic
numbers.map { |n| n * 2 }
# Slow: expensive computation per element
numbers.map { |n| complex_calculation(n) }
# Very slow: I/O per element
urls.map { |url| HTTP.get(url) }
Profile code to identify expensive blocks. Consider caching, memoization, or parallel processing for costly operations. Batch I/O operations instead of performing them per element.
Symbol-to-Proc Performance: Ruby's &:method_name syntax converts symbols to proc objects. This approach has slight overhead compared to explicit blocks but improves readability.
# Symbol-to-proc: clean but slight overhead
names.map(&:upcase)
# Explicit block: marginally faster
names.map { |n| n.upcase }
For simple method calls on small collections, the performance difference is negligible. Prioritize readability unless profiling shows a bottleneck. Explicit blocks provide more flexibility for complex operations.
Memory Efficiency: Each map and filter operation creates a new array. For large datasets, this memory allocation can be significant. Lazy evaluation or mutation-based alternatives reduce memory pressure.
# High memory: multiple intermediate arrays
result = large_array
.map { |x| transform1(x) }
.map { |x| transform2(x) }
.map { |x| transform3(x) }
# Lower memory: lazy evaluation
lazy_result = large_array.lazy
.map { |x| transform1(x) }
.map { |x| transform2(x) }
.map { |x| transform3(x) }
.force
# Lowest memory: in-place mutation (if appropriate)
large_array.map! { |x| transform1(transform2(transform3(x))) }
Consider memory constraints when processing large collections. Lazy evaluation works well for pipelines with filtering or early termination. In-place mutation eliminates copies but modifies the original data.
Reference
Core Operations
| Operation | Ruby Methods | Purpose | Returns |
|---|---|---|---|
| Map | map, collect | Transform each element | New collection, same size |
| Filter | select, filter, find_all | Select matching elements | New collection, same or smaller |
| Inverse Filter | reject | Exclude matching elements | New collection, same or smaller |
| Reduce | reduce, inject | Combine into single value | Single accumulated value |
| Flat Map | flat_map | Map then flatten one level | New flattened collection |
Map Variants
| Method | Signature | Description | Example |
|---|---|---|---|
| map | map { block } | Transform each element | [1,2,3].map { |n| n * 2 } |
| map! | map! { block } | Transform in-place | arr.map! { |n| n * 2 } |
| collect | collect { block } | Alias for map | [1,2].collect { |n| n + 1 } |
| map.with_index | map.with_index { block } | Transform with index | arr.map.with_index { |e,i| [i,e] } |
| transform_values | hash.transform_values { block } | Map hash values | hash.transform_values { |v| v * 2 } |
| transform_keys | hash.transform_keys { block } | Map hash keys | hash.transform_keys(&:to_s) |
Filter Variants
| Method | Signature | Description | Example |
|---|---|---|---|
| select | select { block } | Keep elements where block is true | arr.select { |n| n > 3 } |
| filter | filter { block } | Alias for select | arr.filter { |n| n.even? } |
| reject | reject { block } | Remove elements where block is true | arr.reject { |n| n < 0 } |
| grep | grep(pattern) | Select matching pattern | arr.grep(/^a/) |
| grep_v | grep_v(pattern) | Reject matching pattern | arr.grep_v(/^a/) |
| find_all | find_all { block } | Alias for select | arr.find_all { |n| n > 5 } |
Reduce Variants
| Method | Signature | Description | Example |
|---|---|---|---|
| reduce | reduce(init) { block } | Accumulate with initial value | arr.reduce(0) { |sum,n| sum+n } |
| reduce | reduce { block } | Accumulate using first element | arr.reduce { |prod,n| prod*n } |
| inject | inject(init) { block } | Alias for reduce | arr.inject(0, :+) |
| reduce(symbol) | reduce(:operator) | Use symbol as operation | arr.reduce(:+) |
| sum | sum | Sum numeric elements | [1,2,3].sum |
Predicate Methods
| Method | Purpose | Returns | Short-circuits |
|---|---|---|---|
| all? | Check if all elements match | Boolean | Yes, at first false |
| any? | Check if any element matches | Boolean | Yes, at first true |
| none? | Check if no elements match | Boolean | Yes, at first true |
| one? | Check if exactly one matches | Boolean | No |
| include? | Check if value is present | Boolean | Yes, when found |
| empty? | Check if collection is empty | Boolean | N/A |
Common Patterns
| Pattern | Implementation | Use Case |
|---|---|---|
| Count matches | select { condition }.size | Count elements satisfying condition |
| Find extremum | reduce { |max,n| n > max ? n : max } | Find maximum or minimum |
| Group by attribute | reduce(Hash.new { |h,k| h[k]=[] }) | Partition into categories |
| Build index | reduce({}) { |h,e| h.merge(key => val) } | Create lookup hash |
| Flatten nested | flat_map { |e| e } | Remove one level of nesting |
| Transform pairs | map { |k,v| [k, transform(v)] }.to_h | Transform hash values |
| Filter then aggregate | select { condition }.reduce { operation } | Conditional aggregation |
Performance Characteristics
| Operation | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| map | O(n) | O(n) | Creates new collection |
| select/filter | O(n) | O(k), k ≤ n | Output size varies |
| reduce | O(n) | O(1) | Single accumulator |
| flat_map | O(n*m) | O(n*m) | m = avg elements per map |
| lazy | O(1) initialization | O(1) | Deferred computation |
| map! | O(n) | O(1) | Mutates in place |
Evaluation Strategy
| Strategy | Methods | Behavior | Best For |
|---|---|---|---|
| Eager | map, select, reduce | Immediate evaluation | Small collections, all results needed |
| Lazy | lazy.map.select | Deferred evaluation | Large collections, early termination |
| In-place | map!, select! | Mutates original | Memory constrained, original unneeded |
| Chained | method1.method2.method3 | Sequential intermediate results | Readable pipelines |
Common Gotchas
| Issue | Problem | Solution |
|---|---|---|
| Nil accumulator | Forgetting to return accumulator in reduce | Always return accumulator from block |
| Mutation confusion | Using map instead of map! | Use map! for in-place or reassign result |
| String concatenation | Using + in reduce | Use array accumulator then join |
| Block return value | Wrong value returned from block | Ensure block returns expected value |
| Empty collection | Reduce without initial value on empty | Provide initial value or check empty? |
| Side effects | Mutating elements in map | Keep transformations pure |
| Performance | Unnecessary intermediate arrays | Use lazy for large collections |