Passing Blocks as Arguments

A recent project at Turing involved creating an Enigma machine of sorts that encrypted and decrypted messages by rotating each character to a new one based on a randomly generated key and a date, with different rotators based on the location of the character in the message.

Once I had the functionality working, I realized I had a method in both my decrypt and encrypt classes that did the exact same thing, but in reverse in the case of decrypt. It looked like this:

#In the Encrypt class 
def rotate_message(message) 
  indices_and_rotators = which_rotator(message) 
  new_indices = [] 
  indices_and_rotators.length.times do 
    new_indices << (indices_and_rotators[i][0] + (indices_and_rotators[i][1] % 85)) 
  end 
end 

def rotate_encrypted_message(message) 
  indices_and_rotators = which_rotator(message) 
  new_indices = [] 
  indices_and_rotators.length.times do 
    new_indices << (indices_and_rotators[i][0] - (indices_and_rotators[i][1] % 85)) 
  end 
end

Basically, the indices_and_rotators variable contains a 2D array of the initial index of the character, and the number of characters by which it was to be rotated. As you can see, the only difference in these two methods is that I’m adding the index and rotator together in the case of Encrypt, and subtracting them in the case of Decrypt. In the spirit of DRYing up my code, I wanted to do something about this.

I’ll walk through how it works, but first, spoiler alert — here’s what I ended up with:

#this is in the class from which both Encrypt and Decrypt inherit
def rotate_indices(message, &block) 
  indices_and_rotators = which_rotator(message) 
  @new_indices = [] 
  indices_and_rotators.length.times do |i| 
    @new_indices << block.call(indicies_and_rotators[i][0], indices_and_rotators[i][1] % 85) 
  end 
  ensure_valid_rotator 
end 

#in Encrypt 
def rotate_message(message) 
  rotate_indices(message) do |initial, rotation| 
    initial + rotation 
  end 
end 

#in Decrypt 
def rotate_message(message) 
  rotate_indices(message) do |initial, rotation| 
    initial - rotation 
  end 
end

In the actual code, each of these methods is in a different Class and different files, but I’ve brought them together just to simplify viewing. Here’s how this works:

First, do all the setup (calling the which_rotator method, defined previously, and setting up the new_indices variable as an empty array) in the rotate_indices method so you don’t have to do it twice. Note that the new_indices variable now needs to be an instance variable so that it’s accessible across methods.

In the rotate_indices method, which both Encrypt and Decrypt have access to, call the block as an argument. Do this by calling &block as an argument in the method. You can call it whatever you want, as long as it starts with the ampersand, and you use the same name below when you call the block (line 6 in my example). This block argument does have to be the last one you pass your method.

This turns your block into a Proc that you can call elsewhere, and still give it access to your local variables.

Once you’ve created your Proc, you can call it in the different rotate_message methods. initial references indices_and_rotators[i][0] and rotation references indices_and_rotators[i][1]. Then you can pass each the separate method that they need. (Yes, + and are methods, you can write them the way you can because of Ruby’s syntactic sugar).

This is another brief example. The accumulate method is basically a rewriting of the built in map method.

class Array 
  def accumulate(&block) 
    result = [] 
    self.each do |item| 
      result << block.call(item) 
    end 
    result 
  end 
end 

#In use: 
[1, 2, 3].accumulate do |number| 
  number * number 
end 
# => [1, 4, 9]