Mastering the unary ampersand operator (&object) as a block argument
A complete overview of blocks, block references, and procs.
Feel free to have a look at The yield Keyword in Ruby
if you’re not familiar the yield
keyword and block arguments.
The Ampersand Colon (&:) operator
Ruby proposes a shorthand to call a method on a block argument
[1, 2, 3].map(&:to_f) # => [2.0, 4.0, 6.0]
Is equivalent to
[1, 2, 3].map { |x| x.to_f } # => [2.0, 4.0, 6.0]
By passing &:to_i
to Enumerable#map
, each element of the[1, 2, 3]
array calls to_i
.
It works just fine. It’s magic... So let’s reveal the magic trick! 😉
This operator is also called
Pretzel colon
operator. 🥨 ☕
Symbol#to_proc
When &
prepends an argument during a method call, Ruby expects the object passed as an argument to respond to to_proc
module InspectToProc
def to_proc
puts "to_proc is called upon :#{self.to_s}"
super
end
end
Symbol.prepend InspectToProc
method_name = :to_f
[1, 2, 3].map(&method_name) # => [1.0, 2.0, 3.0]
produces
to_proc is called upon :to_f
Symbol#to_proc
is called when &:to_f
is passed as an argument of map
— which is a method that expects a block.
Then within each iteration of map
, this freshly created proc
will be used as the method’s block.
So let’s re-implement Symbol#to_proc
to illustrate the above explanation
module InspectToProc
def to_proc = lambda {|elem| elem.send(self)} # returns a proc that expects one argument (elem) and calls self (:to_s in this case) upon elem.
end
Symbol.prepend InspectToProc
[1, 2, 3].map(&:to_s) # => ["1", "2", "3"]
is equivalent to
to_string = lambda { |x| x.to_s }
[1, 2, 3].map {|x| to_string.yield(x) } # => ["1", "2", "3"]
to_string
is a lambda proc that converts an object into a string.
Then we pass x
as argument of to_string.yield
(the proc
acting as a block) — which calls to_s
upon x
.
Method#to_proc
Another class that natively responds to #to_proc
is Method
. An instance of Method
is the object-oriented representation of a method.
Instances can only be generated through the method(method_id)
shorthand
Method.new(:puts) # ❌ => NoMethodError: undefined method `new' for Method:Class
method(:puts) # ✅ => #<Method: Object(Kernel)#puts>
Let’s see how to use Method
objects with the &
ampersand operator
def double(x) = x * 2 # I 💚 the shorthand syntax for single line methods!
[1, 2, 3].map &method(:double) # => [2, 4, 6]
is equivalent to
def double(x) = x * 2
[1, 2, 3].map { |x| double(x) } # => [2, 4, 6]
Here x
is passed as an argument of double
(double(x)
) while in the previous section, to_s
was called upon x
(x.to_s
).
It’s also possible to refer to a method encapsulated in another class
require 'date'
datetimes = %w(
2001-02-03T04:05:06+07:00
2002-03-04T04:05:06+08:00
2003-04-05T04:05:06+09:00
)
datetimes.map(&DateTime.method(:strptime)) # => [#<DateTime: 2001-02-03T04:05:06+07:00 ((2451943j,75906s,0n),+25200s,2299161j)>, #<DateTime: 2002-03-04T04:05:06+08:00 ((2452337j,72306s,0n),+28800s,2299161j)>, #<DateTime: 2003-04-05T04:05:06+09:00 ((2452734j,68706s,0n),+32400s,2299161j)>]
is equivalent to
require 'date'
datetimes = %w(
2001-02-03T04:05:06+07:00
2002-03-04T04:05:06+08:00
2003-04-05T04:05:06+09:00
)
datetimes.map do |datetime|
DateTime.strptime(datetime)
end # => [#<DateTime: 2001-02-03T04:05:06+07:00 ((2451943j,75906s,0n),+25200s,2299161j)>, #<DateTime: 2002-03-04T04:05:06+08:00 ((2452337j,72306s,0n),+28800s,2299161j)>, #<DateTime: 2003-04-05T04:05:06+09:00 ((2452734j,68706s,0n),+32400s,2299161j)>]
DateTime.strptime
is a helper provided by the date
standard library to parse string representations of date and time.
DateTime.method(:strptime)
returns the object-oriented representation of strptime
scoped under the DateTime
class.
So the &
ampersand operator uses this instance of Method
like in the previous example.
A concrete implementation: BaseBuilder.to_proc
# Input: Content of a JSON or CSV file uploaded to SFTP server for instance.
raw_users = [{email: 'a@a.com'}, {email: 'b@b.com'}, {email: 'c@c.com'}]
# Let's assume User to be an ActiveRecord model
User = Struct.new(:email, keyword_init: true)
# BaseBuilder: it defines the Builder interface
class BaseBuilder
attr :params
def initialize(user_params) = @params = user_params
# Emulated interface
def build = raise(NotImplementedError)
# Define to_proc at a class level
def self.to_proc = proc { |user_params| new(user_params).build }
end
# UserBuilder
class UserBuilder < BaseBuilder
def build = User.new(params)
end
raw_users.map &UserBuilder # => [#<struct User email="a@a.com">, #<struct User email="b@b.com">, #<struct User email="c@c.com">]
So let’s detail what happens in the above code snippet.
Here we define BaseBuilder
which is a basic implementation of the Builder design pattern:
- it inits
params
(Each line ofraw_users
) - it emulates an interface contract with the
build
method. - it defines
to_proc
at a class level. this method returns aproc
that instantiates a new builder and calls thebuild
instance method on it.
Then we define UserBuilder
which derivates from BaseBuilder
. So in order to be able to pass this class as a block reference to map
, we must comply with the interface contract defined by BaseBuilder
.
This means that we must define UserBuilder#build
. If not, BaseBuilder#build
is called and NotImplementedError
is then raised.
As BaseBuilder.to_proc
returns a proc
and UserBuilder
inherits from BaseBuilder
then a call to raw_users.map &UserBuilder
calls BaseBuilder.to_proc
.
So to define another enumerable builder, you just have to follow 2 rules:
- Make sure that your builder inherits from
BaseBuilder
- Define the
#build
method
Cool! Isn’t it? 😁
Now to finish, let’s have a look at a particular use case where to_proc
is not called when using block reference (&object
) as an argument.
When to_proc is not called
When &
prepends an argument during a method call, Ruby expects the object passed as an argument to respond to to_proc
.
But this is not always the case
module InspectToProc
def to_proc
puts "to_proc is called upon :#{self.to_s}"
super
end
end
Proc.prepend InspectToProc
double = proc {|x| x * 2}
[1, 2, 3].map(&double) # => [2, 4, 6]
Here InspectToProc#to_proc
is never called as double
is already an instance of Proc
.
Conclusion
The Ampersand operator + Block pattern is widely used within the Ruby Standard Library and within multiple well-known gems (devise, etc..).
We all know this pattern through the famous Ampersand Colon operator (&:
).
But with a bit more knowledge, you are now able to extend the possibility of your Ruby code to another level!
Ruby Mastery
We’re currently finalizing our first online course: Ruby Mastery.
Join the list for an exclusive release alert! 🔔
Also, you can follow us on x.com as we’re very active on this platform. Indeed, we post elaborate code examples every day.
💚