places.map(&:city)
I've seen this pattern a lot in Rails and have wondered what exactly it's doing. In researching it, I learned about lambda functions and procs in Ruby, and that in turn led to last week's post about currying functions in Ruby, which relies on basic lambda functionality. Going a bit further, I started looking into the beforementioned pattern, now knowing a bit more about lambdas. At first, I thought I'd understood it, but then I realized that it was more complicated than it initially seemed. The part that was most confusing to me was whether or not the lambda call was supposed to be a symbol, so this is a quick guide for when to use a symbol in this pattern and when not to, as well as an explanation of why.
Here's the TL;DR version if you're not interested in the investigation and the why:
Use Symbol | Don't Use Symbol |
---|---|
Method belongs to the object you are iterating on | Using a Lambda you've created |
Iterating on array of objects and operating on a property |
Okay now let's look at how I got here.
First thing, let's create a lambda function for use throughout this guide. If you read last week's post, this will look very familiar:
half = ->(numerator) {
if numerator % 2 === 0
return numerator / 2
end
return "#{numerator} can't be halved evenly"
}
puts half.call(10)
# => 5
puts half.call(3)
# => "3 can't be halved evenly"
This pattern is super common for iterators, so we'll want a couple of arrays too:
numarry = [0, 1, 2, 3, 4]
textarry = ["a", "b", "c"]
Alright, so what now?
Say we want to half each of the integers in numarry. We should be able to use '&:' in an iterator to call our lambda like this, right?
puts numarry.map(&:half)
Wrong! You get undefined method 'half'
error!
The reason is because half
is a lambda, not a symbol, so you need to change it to:
puts numarry.map(&half)
# => [0, "1 can't be halved evenly, 1, "3 can't be halved evenly", 2]
That makes sense (and seems obvious), so then what's with the symbol pattern at the top? I can't just convert my method to a symbol, so it's not just a matter of preference. Something else must be going on.
I tried a different method, but this time I decided to use a built-in method instead of a local one that I created:
puts textarry.map(&upcase)
And I got an undefined local variable or method 'upcase'
error! So, just for fun, I tried it as a symbol:
puts textarry.map(&:upcase)
# => ["A", "B", "C"]
And voila - it worked! But the question is why?
We know that everything in Ruby is an object, ultimately inheriting from the global class Object
. When you define a method, it's defined with an object as its owner. You can see what the owner of a given method is by using the #owner method:
def double(num)
num * 2
end
puts method(:double).owner
# => Object
but what about our lambda method at the top?
puts half.class
# => Proc
So our lambda is not on the same object as the Integer
that we are calling it on. This gives me an idea! If we think back to the original pattern: places.map(&:city)
the symbol is a property of the Place
class, similar to how upcase
is technically a property of the String
class. Meanwhile, our poor lambda is out there on its own and not associated to anything. There's the difference! Time to test the theory. If I'm correct, then by adding a method to an existing class, I should be able to pass it as a symbol to my iterator.
# modify Integer class to include double method
class Integer
def double
self * 2
end
end
puts numarry.map(&:double)
# => [0, 2, 4, 6, 8]
It worked! After all this investigating, I finally came to the simple guidelines below.
Use Symbol | Don't Use Symbol |
---|---|
Method belongs to the object you are iterating on | Using a Lambda you've created |
Iterating on array of objects and operating on a property |
As a bonus, I gained a stronger understanding of how Ruby works under the hood, which is always a good thing!
Top comments (0)