Disclosure: I am currently a student in Launch School and most of this information has been sourced from their curriculum
When learning Ruby, many of you will eventually, if it has not happened yet, come across a concept that just doesn’t sit right in your head. You start to reread the description of this concept in whatever source you are using over and over again, but for some reason you just cannot grasp it. Relax. It’s okay. This is a perfectly normal phenomenon because if you are new to programming, this is probably the first time your brain has ever had to digest a concept like this and for that reason it takes time for you to fully develop a mental model. The point of this post is to help create a better mental model for the new programmer when they come across using the ampersand (&) in terms of blocks. If you are in Launch School, this post is aimed to supplement learning in Lesson 1 of RB130.
I believe the best way to simplify this mental model is to adhere to two (very simplified) rules:
- In the context of a method DEFINITION, putting ampersand in front of the last parameter will indicate that the method you are defining may or may not take a block, giving us a name to refer to this block if it is passed in
- In the context of method INVOCATION, putting ampersand in front of the last argument tells Ruby to convert that argument to a Proc object if necessary and then the ampersand will make the Proc object into a block that can be referenced in the body of the method
Read those two lines again. Again. Refer to them every time you get confused. The ampersand is doing something different when you use it in front of a parameter during a method definition and is doing something else when you use it in front of an argument you are passing in during a method call.
Let’s take apart the first simplification. It says that using the ampersand in front of your last parameter for your method definition will indicate that the method “may or may not take a block, giving us a name to refer to this block if it is passed in”. “May or may not”. This means that we should be able to define a method with an argument with the ampersand in front of it, but NOT HAVE to pass anything to that method. In other words, that argument is optional. This is demonstrated below:
As we can see the method was defined with one parameter, that had an & prepended to it. Because the & was prepended and it was the last parameter defined, passing an argument for that parameter is optional and therefore, the code above runs without anything passed to the method.
The next part of the statement referred to idea that if we do pass in that optional block argument, we would have a way to reference it within the method body. The way we reference the block in this scenario would be that the parameter name after the ampersand(‘block’ in the above code) would be treated as a local variable within the method body. This is useful because this allows the method to be more flexible with the block passed in, rather than just yielding to it. An example of when you might need to be more flexible would be if you wanted to pass that block to another method within your original method body. Examine the following code:
On line 10 we create a Proc object and assign it to the local variable ‘a’. When we call the method on line 12 and put the ampersand in front of our local variable which is referencing a Proc object, Ruby will first check to see whether the object following the ampersand (the object that ‘a’ is referencing here), is a Proc. If it is a Proc, the ampersand will convert that Proc into a block that can be referenced in method body. So now, the local variable ‘block’ on line 3 that is being passed into the method ‘method2’ is referencing the following block derived from the Proc object:
This is why when we call the local variable ‘block’ on line 7 that is referencing the block that the ampersand created from our Proc object, it is able to be called and ‘block content’ is outputted, returning nil. If however, we had not used the & in this scenario, watch what happens when we try to pass in the local variable ‘a’ without the ampersand prepended to it.
This is because our method definition does not define any parameters that aren’t blocks. So when we try to pass in a Proc object, Ruby will register that as an argument that is not a block and will raise an error because we did not define any non-block parameters in our method definition for ‘method’. The ampersand (&) is crucial to convert that Proc object into a block.
So to recap, what happens when we pass in an argument with the ampersand (&) prepended to it is, Ruby will check to see if the object following the ampersand is a Proc or not. If it is a Proc, the ampersand will convert that Proc object into a block that can be passed around within the method body. If it is not a Proc, Ruby will call ‘to_proc’ on the object to attempt to turn it into a Proc object so that the ampersand can convert it into a block. If there is no ‘to_proc’ method for the object, an error will be raised. If there is, the object will be converted to a Proc object by RUBY and then the AMPERSAND will turn that Proc object into a block that can be referenced within the method body.
This is the basis behind why one can write simplified code such as:
Let’s break down what is happening here. If we refer to our two rules, we know the ampersand in this context is dealing with a method invocation because we are calling the method map on the receiving array object ‘[1, 2, 3, 4]’. So the following steps will happen:
- Ruby will check to see if the object following the ampersand is a Proc object. In this case, ‘:to_s’ is a symbol object, not a Proc object.
- Since, it is not a Proc object, Ruby will call ‘to_proc’ on the object. Luckily, there is a Symbol#to_proc method that will turn the symbol :to_s into a Proc object encompassing the implementation of the ‘to_s’ method and the ampersand will turn that Proc object into a block that looks like:
3. Finally, when the map method iterates over each element in the array and returns a new array with the return value of the block for each iteration, it will be able to yield the block above for each element in the array. Hence, on the first iteration, it will yield(1) passing 1 into the block above and the block above returning the string ‘1’. The map method adds that return value to a new array and then goes onto the next iteration, passing 2 into the block and so on and so on until all iterations are complete and the method returns an array of all the original elements converted to strings.
*Keep in mind, this shorthand will only work for methods that do not take any arguments.
I hope this helps to provide some clarity on how the unary ampersand interacts with your program when dealing with blocks in this context. Keep in mind the unary ampersand is different from the binary ampersand, which is not discussed here. Remember, when in doubt, try to refer to the two overly simplified rules!