A Case for Using Procs
I’ve read about and practiced working with Ruby procs – all with contrived examples. That’s all well and good, but for me, the best learning comes with a heaping helping of context, which has been sorely missing from most of the contrived examples. I finally got some good context today when the need for a proc sprung up organically in a code challenge I was working on. So now I get it. I have a much stronger foothold on procs and when to use them. Hopefully seeing my process here will help build context for when you may be able to use a proc to solve a similar problem.
The Challenge: Building a Message Cipher
Here’s the challenge: I’m building a message cipher – a small program that will encode and decode messages. When it encodes, it takes a letter argument and an offset. It adds the offset to the input letter and outputs letter corresponding to that location. For example:
With an offset of 0, all letters line up
offset = 0
input = a
+0 +1 +2 +3 +4 +5 ...
a b c d e f ...
a + 0 = a
With an offset of 3, all numbers slide down 3 places
offset = 3
input = a
+0 +1 +2 +3 +4 +5 ...
a b c d e f ...
a + 3 = d
offset = 3
input = c
+0 +1 +2 +3 +4 +5 ...
c d e f g h ...
c + 3 = f
The Encoder
Here’s my original encode
method. For the sake of simplicity in explaining procs, this encoder does not handle wrapping the alphabet for letters exceeding “z”.
ALPHABET = ('a'..'z').to_a
def encode(message, offset)
output = []
message.chars.each do |letter|
new_index = ALPHABET.index(letter) + offset
output << ALPHABET[new_index]
end
output.join()
end
# Outputting the results:
cipher = Cipher.new
p cipher.encode('ilikekitties', 3)
#> lolnhnlwwlhv
The Decoder
After writing the encoder, I realized that the only difference between the process of encoding and decoding was the use of +
or -
. Instead of creating a nearly identical decode
method, I decided to extract the encode/decode logic out into their own methods and leave the rest of the process in a method called processor
.
def processor(message, offset, adjusted_index_proc)
output = []
message.chars.each do |letter|
#
# This part will need to become generic
# so it can both encode and decode.
#
end
output.join()
end
def encode(message, offset)
#
# Encoding-specific code will go here,
# then we'll call the processor method.
#
end
def decode(message, offset)
#
# Decoding-specific code will go here,
# then we'll call the processor method.
#
end
Then I realized that in order for the encode/decode methods to work, they’ll have to access the value of the letter
iterator happening in the message.chars.each do |letter|...
block inside the processor
method. Yikes. So how do you do that?
Procs! Tada! I found my own completely non-contrived use for a proc. At last. Here’s how it plays out:
Incorporating the proc logic
I created a new proc for both the encode
and decode
methods:
def encode(message, offset)
proc = Proc.new #...
processor(message, offset, proc)
end
def decode(message, offset)
proc = Proc.new #...
processor(message, offset, proc)
end
And then passed the proc into the processor
method as an argument.
def processor(message, offset, adjusted_index_proc)
# ...
end
I needed the proc inside the each
loop to:
- take the value of the current message input letter
- get the index of where it is stored in the
ALPHABET
array - add the offset value (3) to that index
- return the new index number for the encoded letter
So the
encode
anddecode
methods with their procs look like this:
def encode(message, offset)
proc = Proc.new { |letter| ALPHABET.index(letter) + offset }
processor(message, offset, proc)
end
def decode(message, offset)
proc = Proc.new { |letter| ALPHABET.index(letter) - offset }
processor(message, offset, proc)
end
The new processor
method takes the proc and .call()
s it along with a letter
argument for each letter of the input message inside the each
loop. That new index is then used to get the value of the new letter from the ALPHABET
array and then the new letter is pushed into the output
array.
def processor(message, offset, adjusted_index_proc)
output = []
message.chars.each do |letter|
new_index = adjusted_index_proc.call(letter)
output << ALPHABET[new_index]
end
# ...
end
Lastly, the output array is joined back into a string and we get our encoded/decoded message.
def processor(message, offset, adjusted_index_proc)
#...
output.join()
end
Outputting the Results
# Outputting the results
cipher = Cipher.new
p cipher.encode('ilikekitties', 3)
#> lolnhnlwwlhv
p cipher.decode('lolnhnlwwlhv', 3)
#> ilikekitties
Putting it all Together
class Cipher
ALPHABET = ('a'..'z').to_a
def processor(message, offset, adjusted_index_proc)
output = []
message.chars.each do |letter|
new_index = adjusted_index_proc.call(letter)
output << ALPHABET[new_index]
end
output.join()
end
def encode(message, offset)
proc = Proc.new { |letter| ALPHABET.index(letter) + offset }
processor(message, offset, proc)
end
def decode(message, offset)
proc = Proc.new { |letter| ALPHABET.index(letter) - offset }
processor(message, offset, proc)
end
end
cipher = Cipher.new
p cipher.encode('ilikekitties', 3)
#> lolnhnlwwlhv
p cipher.decode('lolnhnlwwlhv', 3)
#> ilikekitties
Though this is not the most streamlined or elegant cipher code base, it has been useful to me in building my understanding of procs. I hope it’s been useful to you too!