DEV Community

Cover image for What's up with the & in Ruby and some background on procs
wlytle
wlytle

Posted on

What's up with the & in Ruby and some background on procs

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
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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!

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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) }
Enter fullscreen mode Exit fullscreen mode

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] 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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).

Discussion (0)