CrackedRuby CrackedRuby

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