So there I was, reading The Well-Grounded Rubyist (a fantastic book, by the way!), trying to implement my own versions of Ruby’s each
and map
methods. The code looked simple enough. I added my_each
to the Array
class, and then wrote my_map
to use my_each
with a block. Here’s what I had:
class Array def my_each c = 0 until c == size yield self[c] c += 1 end self end def my_map acc = [] my_each { |e| acc << yield e } # This line broke everything! acc end end
But when I ran it, I got this infuriating error:
page185a.rb:14: syntax error, unexpected local variable or method, expecting '}' my_each { |e| acc << yield e }
The error pointed to the yield e
part. Why? I thought I followed the book exactly! I tried splitting the code into different lines, rewriting the block—nothing worked. Turns out, the issue was something I’d never considered: Ruby’s parser gets confused when yield
is used with certain operators like <<
.
The Fix That Saved My Sanity
After hours of Googling and yelling at my screen, I found the solution: wrap yield e
in parentheses. Yes, really. That’s it.
Here’s the corrected my_map
method:
def my_map acc = [] my_each { |e| acc << (yield e) } # Parentheses fix the ambiguity! acc end
Why does this work?
Ruby’s parser thought I was trying to call << yield
as a method on e
, which makes no sense. Adding parentheses clarifies that yield e
is a single value being passed to <<
. Lesson learned: when in doubt, add parentheses to yield
in blocks!
Now, this works perfectly:
names = ["David", "Alan", "Black"] p names.my_map { |name| name.upcase } # => ["DAVID", "ALAN", "BLACK"]
Leveling Up Adding More Methods
Once I fixed the yield
issue, I decided to flex my new Ruby skills by adding more methods inspired by Ruby’s Enumerable
module. Here’s what I built:
my_map
Without a Block Returns an Enumerator
Ruby methods like map
return an Enumerator
if no block is given. Let’s replicate that:
def my_map return to_enum(:my_map) unless block_given? # No block? Return Enumerator! acc = [] my_each { |e| acc << (yield e) } acc end
Example:
map_enum = [1, 2, 3].my_map p map_enum.each { |n| n * 2 } # => [2, 4, 6]
In-Place my_map!
Method
Why not modify the array directly, like map!
?
def my_map! return to_enum(:my_map!) unless block_given? my_each_with_index { |e, i| self[i] = yield(e) } # Update the array in place self end
Example:
numbers = [1, 2, 3] numbers.my_map! { |n| n * 10 } p numbers # => [10, 20, 30]
Filtering with my_select
Let’s filter elements based on a block condition:
def my_select return to_enum(:my_select) unless block_given? acc = [] my_each { |e| acc << e if yield(e) } # Collect elements where the block is truthy acc end
Example:
p [10, 20, 30, 40].my_select { |n| n > 25 } # => [30, 40]
The Swiss Army Knife: my_reduce
This one was tricky! I wanted to replicate reduce
/inject
, supporting both symbols (like :+
) and blocks:
def my_reduce(initial = nil, sym = nil, &block) acc = initial.nil? ? first : initial # Handle initial value start_idx = initial.nil? ? 1 : 0 if block my_each_with_index(start_idx...) { |e, i| acc = block.call(acc, e) } elsif sym my_each_with_index(start_idx...) { |e, i| acc = acc.send(sym, e) } else raise ArgumentError, "You must provide a block or a symbol" end acc end
Examples:
p [1, 2, 3].my_reduce(:+) # => 6 (sum) p [1, 2, 3].my_reduce(10, :+) # => 16 (sum with initial value)
Final Thoughts
- Parentheses Matter with
yield
: Ruby’s parser isn’t perfect. When combiningyield
with operators like<<
, clarify your intent with(yield e)
. - Emulate Ruby’s Patterns: Returning
Enumerator
objects when no block is given makes your methods more flexible and idiomatic. - Iteration is Powerful: By building
my_each
,my_map
,my_select
, andmy_reduce
, I finally grasped how Ruby’s iteration methods work under the hood.
If you’re new to Ruby like me, don’t fear yield
just remember to add those parentheses! And hey, try re implementing methods yourself.