Scrolling through API docs and Stack Overflow threads on Ruby inevitably reveals a number of unexplained &
s floating around. Being new to Ruby and engineering in general, I thought I'd dig into what the ampersand, known as the safe operator, is and does.
The safe operator calls .to_proc
on whatever it proceeds. But what, praytell, is a proc and what does it mean to become one? Before we get into &
, we have to briefly fall down the rabbit hole of the Proc
class.
Procs
On the surface, this isn't too terribly complicated. An instance of the Proc
class is simply an executable block of code that can be stored in a variable to be called later or even passed as an argument to a method. A block of code is a grouping of statements called by a method. It is surrounded by { }
or the do end
keywords; think about what comes after .each
.
cube = Proc.new { |i| i**3 }
cube.call(2)
# => 8
Note the .call
method needed to call the proc instance which can also be accomplished with the shorthand cube.(2)
or cube[2]
.
As mentioned, procs can also be used to pass a block as an argument of a method.
def i_take_a_proc(proc_arg)
proc_arg.call
end
proc_arg = Proc.new { puts "I'm a block being passed as an argument!" }
i_take_a_proc(proc_arg)
# => I'm a block being passed as an argument!
Now we can take this new (micro)superpower and apply the safe operator. Remember the safe operator simply calls .to_proc
on its operand.
def make_proc(&block)
block
end
hello = make_proc { |i| puts "#{i} says hello!" }
hello.call("The Moon")
# => The Moon says hello!
When Ruby sees the &
, it expects a proc to follow. Since we provide a block, Ruby will try to coerce it into a proc by calling .to_proc
on the block.
Special Behaviors of Procs
Now that we have a handle on what procs are, let's talk about some of the properties that make them such beautiful and unique snowflakes. First off, procs (and blocks) are a type of closure, meaning that they remember the environment in which they are created. They essentially form a snapshot of the variables in their scope and will use that snapshot when they are called even if those variables have been reassigned.
def run_proc(say_hi)
hi = "Hello"
say_hi.call
end
hi = "Hola"
say_hi = Proc.new { puts hi }
run_proc(say_hi)
# => Hola
This will puts Hola
to the screen NOT Hello
because that was the value of the hi
variable that our new proc had access to when it was created and - because procs are closures - it holds onto that assignment of the variable.
Another interesting feature of procs is how they handle an explicit return
. Rather than simply returning from their own block they will return from the surrounding context as well.
proc_that_returns = Proc.new { return "I'm Free!" }
proc_that_returns.call
Running this code outside of a method will produce a LocalJumpError
because it is trying to return out of the top-most context.
Finally, procs are surprisingly laidback objects in the Ruby world. They are comfortable ignoring nil values and are not picky about the number of arguments they are given.
proc_with_two_args = Proc.new { |x, y| x.even? }
proc_with_two_args.call(6)
=> true
This proc expects two arguments: |x, y|
. Even though it only receives one, no ArgumentError
is raised!
Lambdas, Briefly
It's hard to dive into procs without talking about lambdas, but I'm here to talk about the safe operator so I'll make it quick. A lambda is a type of proc with slightly different behaviors. Namely, they do not return out of the surrounding scope with an explicit return, and they are particular about the number of arguments they accept. That's all!
lambda_with_two_args = lambda { |x, y| x.even? }
lambda_with_two_args.call(6)
=> ArgumentError
The Safe Operator, Finally
So, what does this all mean for our friend the safe operator, &
? Well, using what we now know, we can employ it to the benefit of some nice syntactic sugar.
This is most commonly seen when &
is called on a symbol in the argument of an enumerator.
animals = ["dog", "platypus", "scarab"]
animals.map(&:length)
=> [3, 8, 6]
#This is equivalent to: animals.map {|animal| animal.length}
This is a neat trick, but what's going on here? Well, the safe operator indicates that Ruby expects a proc to follow it. Instead, it finds a symbol (:length
) so it calls to_proc
on the symbol, resulting in something like this:
{ |a| a.send(self) }
In this case, self
is the symbol :length
, so Ruby calls the length
method on the object that is calling (&:length)
. To be fair, this is likely a gross oversimplification of what is actually going on, but it helps clear things up a bit.
Another bit of syntactic wizardry the safe operator offers is a kind of failsafe. Because the safe operator is comfortable with missing arguments and nil data, it can be used when nil data may be present and you don't want it to break your code.
array = [1, 2, 3, nil, 5]
array.map { |e| e.even? }
=> NoMethodError
array.map { |e| e&.even? }
[false, true, false, nil, false]
This is a bit cleaner than writing lengthy if
statements to check if all your data exist before using it. That said, it should be used carefully if having nil values in your data will be a major problem later. It can also be used when chaining method calls together. The safe operator will return nil
when it finds the first nil object.
#No safe operator :(
if genre && genre.artist && genre.artist.album
...
end
#With safe operator
if genre&.artist&.album
...
end
Wrap up
Safe operators and procs may not change your life or even your code, but they can clean things up a bit. At the very least, they make it look like you know what you're doing (maybe not always a good thing).
Top comments (0)