How to Build Your Own Map Method in Ruby
One of Ruby's most powerful features is the ability to extend the language by writing your own methods. In this article, we'll explore how Ruby's popular map method works and build our own implementation from scratch.
Why Build Our Own Map Method?
Ruby already has a map method, so why create our own implementation? Here are several reasons:
- Learning purpose: Better understand Ruby's internal workings
- Customization: Extend the map method for your specific needs
- Performance optimization: Create optimized versions for specific use cases
- Technical interview preparation: These types of questions are frequently asked
Understanding the Map Method Logic
The map method transforms each element of a collection using a block and returns a new array:
# Ruby's built-in map method
[1, 2, 3, 4].map { |x| x * 2 }
# => [2, 4, 6, 8]Our First Implementation
Let's start with the simplest map implementation:
class Array
  def my_map
    result = []
    each { |element| result << yield(element) }
    result
  end
end
# Let's test it
numbers = [1, 2, 3, 4, 5]
doubled = numbers.my_map { |x| x * 2 }
puts doubled.inspect  # [2, 4, 6, 8, 10]This implementation works, but there's a problem: we get an error when no block is provided.
Adding Enumerator Support
Ruby's original map method returns an Enumerator when no block is given. Let's add this to our implementation:
class Array
  def my_map
    # Return Enumerator if no block is given
    return enum_for(:my_map) unless block_given?
    
    result = []
    each { |element| result << yield(element) }
    result
  end
end
# Test: With block
[1, 2, 3].my_map { |x| x ** 2 }  # => [1, 4, 9]
# Test: Without block (returns Enumerator)
enum = [1, 2, 3].my_map
enum.with_index { |x, i| "#{i}: #{x}" }  # => ["0: 1", "1: 2", "2: 3"]More Flexible Approach: Using Modules
Instead of just extending the Array class, let's create a module:
module MyEnumerable
  def my_map
    return enum_for(:my_map) unless block_given?
    
    result = []
    each { |element| result << yield(element) }
    result
  end
  
  def my_map_with_index
    return enum_for(:my_map_with_index) unless block_given?
    
    result = []
    each_with_index { |element, index| result << yield(element, index) }
    result
  end
end
# Include in different classes
class MyArray
  include MyEnumerable
  
  def initialize(items)
    @items = items
  end
  
  def each
    return enum_for(:each) unless block_given?
    @items.each { |item| yield(item) }
  end
end
# Usage
my_array = MyArray.new([1, 2, 3])
result = my_array.my_map { |x| x * 3 }
puts result.inspect  # [3, 6, 9]Performance Optimization
An optimized version for large datasets:
module FastEnumerable
  def fast_map
    return enum_for(:fast_map) unless block_given?
    
    # Pre-allocate array with known size
    if respond_to?(:size)
      result = Array.new(size)
      each_with_index { |element, i| result[i] = yield(element) }
    else
      result = []
      each { |element| result << yield(element) }
    end
    
    result
  end
endError Handling
Let's add error handling for a robust implementation:
module SafeEnumerable
  def safe_map
    return enum_for(:safe_map) unless block_given?
    
    result = []
    each_with_index do |element, index|
      begin
        result << yield(element)
      rescue => e
        warn "Error at index #{index}: #{e.message}"
        result << nil  # or another default value
      end
    end
    
    result
  end
endReal-World Example
A custom map method for processing API data:
class APIResponse
  include MyEnumerable
  
  def initialize(data)
    @data = data
  end
  
  def each
    return enum_for(:each) unless block_given?
    @data.each { |item| yield(item) }
  end
  
  def transform_users
    my_map do |user_data|
      {
        id: user_data['id'],
        name: user_data['full_name']&.strip,
        email: user_data['email']&.downcase,
        created_at: Time.parse(user_data['created_at'])
      }
    end
  end
end
# Usage
response = APIResponse.new([
  {'id' => 1, 'full_name' => 'John Doe', 'email' => 'JOHN@EMAIL.COM'},
  {'id' => 2, 'full_name' => 'Jane Smith', 'email' => 'JANE@EMAIL.COM'}
])
users = response.transform_usersBenchmark: Performance Comparison
Let's test our implementation's performance:
require 'benchmark'
array = (1..100_000).to_a
Benchmark.bm do |x|
  x.report("Built-in map:") { array.map { |x| x * 2 } }
  x.report("Custom my_map:") { array.my_map { |x| x * 2 } }
endTesting Our Implementation
Don't forget to test your custom methods:
require 'minitest/autorun'
class TestMyMap < Minitest::Test
  def setup
    @array = [1, 2, 3, 4, 5]
  end
  
  def test_my_map_with_block
    result = @array.my_map { |x| x * 2 }
    assert_equal [2, 4, 6, 8, 10], result
  end
  
  def test_my_map_without_block
    enum = @array.my_map
    assert_instance_of Enumerator, enum
  end
  
  def test_my_map_preserves_original
    original = @array.dup
    @array.my_map { |x| x * 2 }
    assert_equal original, @array
  end
endBest Practices
When implementing custom enumerable methods:
- Always check for blocks: Use block_given?and return enumerators appropriately
- Don't mutate the original: Create new collections instead
- Handle edge cases: Empty collections, nil values, etc.
- Consider performance: Pre-allocate arrays when size is known
- Add comprehensive tests: Cover all scenarios
- Document your methods: Explain parameters and return values
Conclusion
Building your own map method is an excellent exercise for understanding Ruby's internals. What we've learned:
- ✅ Basic mapimplementation
- ✅ Adding Enumeratorsupport
- ✅ Writing reusable code with modules
- ✅ Error handling and performance optimization
- ✅ Real-world usage examples
- ✅ Advanced features like lazy evaluation
Which Ruby enumerable method would you most want to implement next, and why? Drop your answers in the comments!