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]