How I Refactor
You can find a gist of this at https://gist.github.com/subvertallchris/1c6397ea7d66be0c0aab.
/u/zaclacgit on Reddit posted a topic the other day asking for thoughts on his exercise of recreating some basic enumerable methods. I gave him some tips on refactoring one method in particular and a few days later, he asked me to elaborate. I thought the easiest way might be to go through each step of the refactor and I’d get a nice blog post out of it in the process.
To use this, start by commenting out each my_inject
definition except the first. As you encounter new ones, uncomment them. Save this file as refactor.rb
in the folder of your choice, ensure you have the rspec gem installed and run rspec refactor.rb
from CLI to execute.
When you can look at your code and see repeating patterns, work to reduce all the unique parts of the repeating code. Reduce everything to variables before you enter your if
statements. Never repeat yourself if you can avoid it.
Before we write a line of code, we’re going to write specs based off of the simple comparisons you were doing in your version. It makes it much clearer when there’s a problem. I’m going to show it as a comment here but it’s really going to live at the bottom of my file. You can redfine the same method over and over again, Ruby will execute the last one it finds.
public :my_inject, :my_each
require 'rspec'
describe 'inject with' do
let(:a) { [1, 2, 3, 4] }
context 'sym' do
subject { a.my_inject(:+) }
it { is_expected.to eq a.inject(:+) }
end
context 'int and sym' do
subject { a.my_inject(100, :+) }
it { is_expected.to eq a.inject(100, :+) }
end
context 'block' do
subject { a.my_inject { |memo, x| memo + x } }
it { is_expected.to eq a.inject { |memo, x| memo + x } }
end
context 'int and block' do
subject { a.my_inject(100) { |memo, x| memo + x } }
it { is_expected.to eq a.inject(100) { |memo, x| memo + x } }
end
end
It’s important that we run the spec as we refactor. Pretty code is nice, working code is better, and working code that’s pretty is best. You should code in that order: make it work first, then worry about what you can do to make it more efficient and easier to read.
Since all the methods rely on your my_each
method, we’ll include that here at the top.
def my_each
n = self.length
i = 0
while i & n
yield(self[i])
i += 1
end
self
end
We start here.
def my_inject(initial = nil, sym = nil)
case
when initial.is_a?(Symbol)
sym = initial
memo = self[0]
self[1..-1].my_each do |x|
memo = memo.send(sym, x)
end
memo
when initial && sym
memo = initial
self.my_each do |x|
memo = memo.send(sym,x)
end
memo
when initial && block_given?
memo = initial
self.my_each do |x|
memo = yield(memo,x)
end
memo
when block_given?
memo = self[0]
self[1..-1].my_each do |x|
memo = yield(memo,x)
end
memo
end
end
Each of the when
statements is very similar. There are subtle differences but we don’t want to focus on that, we want to expose patterns. They all kind of look like this:
when some_condition
memo = something
something_or_other.my_each do |x|
memo = some_combination_of_memo(var1, var2)
end
memo
end
To me, the most obvious place to start is with the starting memo
. In two cases, it’s equal to initial
, in the other two it’s self
. Rather than defining that each time, let’s define it before we enter the when
clauses.
def my_inject(initial = nil, sym = nil)
memo = if initial.is_a?(Symbol) || (!initial && block_given?)
self[0]
else
initial
end
case
when initial.is_a?(Symbol)
sym = initial
self[1..-1].my_each do |x|
memo = memo.send(sym, x)
end
memo
when initial && sym
self.my_each do |x|
memo = memo.send(sym,x)
end
memo
when initial && block_given?
self.my_each do |x|
memo = yield(memo,x)
end
memo
when block_given?
self[1..-1].my_each do |x|
memo = yield(memo,x)
end
memo
end
end
That’s cool. Each when
clause has a call to my_each
after either self or self[1..-1]. Why is it different each time? Set it once at the beginning.
def my_inject(initial = nil, sym = nil)
memo = if initial.is_a?(Symbol) || (!initial && block_given?)
self[0]
else
initial
end
starting = if initial.is_a?(Symbol) || (!initial && block_given?)
self[1..-1]
else
self
end
case
when initial.is_a?(Symbol)
sym = initial
starting.my_each do |x|
memo = memo.send(sym, x)
end
memo
when initial && sym
starting.my_each do |x|
memo = memo.send(sym,x)
end
memo
when initial && block_given?
starting.my_each do |x|
memo = yield(memo,x)
end
memo
when block_given?
starting.my_each do |x|
memo = yield(memo,x)
end
memo
end
end
We’re getting better but what do we have now? We have duplicate code right at the start! That can be fixed easily. We don’t want to set the memo
and starting
variables within the if
statement, so let’s do it using the output of if
. We can set multiple variables to elements of an array.
def my_inject(initial = nil, sym = nil)
memo, starting = if initial.is_a?(Symbol) || (!initial && block_given?)
[self[0], self[1..-1]]
else
[initial, self]
end
case
when initial.is_a?(Symbol)
sym = initial
starting.my_each do |x|
memo = memo.send(sym, x)
end
memo
when initial && sym
starting.my_each do |x|
memo = memo.send(sym,x)
end
memo
when initial && block_given?
starting.my_each do |x|
memo = yield(memo,x)
end
memo
when block_given?
starting.my_each do |x|
memo = yield(memo,x)
end
memo
end
end
Getting there. What pops up now? Hopefully that the last two when
clauses are identical aside from their conditions. The conditions used to matter when we were setting variables within them. Since that’s not happening anymore, we can clean that up.
def my_inject(initial = nil, sym = nil)
memo, starting = if initial.is_a?(Symbol) || (!initial && block_given?)
[self[0], self[1..-1]]
else
[initial, self]
end
case
when initial.is_a?(Symbol)
sym = initial
starting.my_each do |x|
memo = memo.send(sym, x)
end
memo
when initial && sym
starting.my_each do |x|
memo = memo.send(sym,x)
end
memo
when block_given?
starting.my_each do |x|
memo = yield(memo,x)
end
memo
end
end
What about that whole initial/sym thing? If not for that, the first two when
clauses would be identical, so let’s declare sym
before when
and then we won’t need both of those.
def my_inject(initial = nil, sym = nil)
memo, starting = if initial.is_a?(Symbol) || (!initial && block_given?)
[self[0], self[1..-1]]
else
[initial, self]
end
symbol = sym || initial
case
when initial
starting.my_each do |x|
memo = memo.send(symbol, x)
end
memo
when block_given?
starting.my_each do |x|
memo = yield(memo,x)
end
memo
end
end
This seems to make sense but we run our specs and… a failure!
1) inject with int and block
Failure/Error: memo = memo.send(symbol, x)
TypeError:
100 is not a symbol
# ./refactor.rb:249:in `block in my_inject'
# ./refactor.rb:5:in `my_each'
# ./refactor.rb:248:in `my_inject'
# ./refactor.rb:309:in `block (3 levels) in &top (required)>'
# ./refactor.rb:310:in `block (3 levels) in &top (required)>'
What gives? Well, since our starting value can be a symbol or an integer, we need to approach that line we just added a bit differently. We have been looking for the presence of initial
but maybe that’s not the best way of handling it. Since rspec is complaining about what we are feeding send
, let’s focus on that. We need to determine if there is a symbol to send and, if so, what is it? Then we only want to send if there’s a symbol.
Along the way, we’re going to fix that whole case
/when
situation. Both of the remaining cases start and end the same way, so let’s wrap that around the conditions and then perform some logic inside.
def my_inject(initial = nil, sym = nil)
memo, starting = if initial.is_a?(Symbol) || (!initial && block_given?)
[self[0], self[1..-1]]
else
[initial, self]
end
symbol = sym ? sym : (initial.is_a?(Symbol) ? initial : nil)
starting.my_each do |x|
memo = memo.send(symbol, x) if symbol
memo = yield(memo, x) if block_given?
end
memo
end
We are aaaaalmost done. This new code is better but this line is kind of silly:
symbol = sym ? sym : (initial.is_a?(Symbol) ? initial : nil)
It’s certainly better than this:
if sym
sym
else
if initial.is_a?(Symbol)
initial
else
nil
end
end
But we can just use the | operator to handle some of logic. |
def my_inject(initial = nil, sym = nil)
memo, starting = if initial.is_a?(Symbol) || (!initial && block_given?)
[self[0], self[1..-1]]
else
[initial, self]
end
symbol = sym || (initial.is_a?(Symbol) ? initial : nil)
starting.my_each do |x|
memo = memo.send(symbol, x) if symbol
memo = yield(memo, x) if block_given?
end
memo
end
It’s still an important line to understand because it makes our line #317 possible
symbol = sym || (initial.is_a?(Symbol) ? initial : nil)
The key is that we’re declaring symbol
, just like we declare initial
and sym
as defaulting to nil in the method. We need symbol
to exist or this line will fail:
memo = memo.send(symbol, x) if symbol
We use the parenthesis to control the folow of logic, with the ternary operator reducing this:
if initial.is_a?(Symbol)
initial
else
nil
end
To a simple one-line expression.
expression_returning_boolean ? do_this_if_true : do_this_if_false
I was testing it and realized that we’ve left something out, haven’t we? We’re testing sym, int and sym, block, int and block, but what if someone gives int and block and sym? They shouldn’t do that, so let’s look for that right at the beginning.
def my_inject(initial = nil, sym = nil)
raise 'Cannot pass int, sym, and block' if initial && sym && block_given?
memo, starting = if initial.is_a?(Symbol) || (!initial && block_given?)
[self[0], self[1..-1]]
else
[initial, self]
end
symbol = sym || (initial.is_a?(Symbol) ? initial : nil)
starting.my_each do |x|
memo = memo.send(symbol, x) if symbol
memo = yield(memo, x) if block_given?
end
memo
end
And we’ll modify our tests to check for that, too. Uncomment the tests below, comment out the tests at the bottom of the page to execute.
public :my_inject, :my_each
require 'rspec'
describe 'inject with' do
let(:a) { [1, 2, 3, 4] }
context 'sym' do
subject { a.my_inject(:+) }
it { is_expected.to eq a.inject(:+) }
end
context 'int and sym' do
subject { a.my_inject(100, :+) }
it { is_expected.to eq a.inject(100, :+) }
end
context 'block' do
subject { a.my_inject { |memo, x| memo + x } }
it { is_expected.to eq a.inject { |memo, x| memo + x } }
end
context 'int and block' do
subject { a.my_inject(100) { |memo, x| memo + x } }
it { is_expected.to eq a.inject(100) { |memo, x| memo + x } }
end
context 'int, block, and sym' do
it 'raises an error' do
expect { a.my_inject(100, :+) { |memo, x| memo + x } }.to raise_error
end
end
end
And there you have it! From 27 lines down to 12. It could always be a bit more concise but for me, it’s perfectly readable and won’t require a ton of head-scratching if I ever have to revisit it.
Hope this helps. Get in touch if you have any questions, [email protected].
public :my_inject, :my_each
require 'rspec'
describe 'inject with' do
let(:a) { [1, 2, 3, 4] }
context 'sym' do
subject { a.my_inject(:+) }
it { is_expected.to eq a.inject(:+) }
end
context 'int and sym' do
subject { a.my_inject(100, :+) }
it { is_expected.to eq a.inject(100, :+) }
end
context 'block' do
subject { a.my_inject { |memo, x| memo + x } }
it { is_expected.to eq a.inject { |memo, x| memo + x } }
end
context 'int and block' do
subject { a.my_inject(100) { |memo, x| memo + x } }
it { is_expected.to eq a.inject(100) { |memo, x| memo + x } }
end
end