Elegantly and-ing Array
Recently, I had a problem of how to evaluate and in a data
structure.
Specifically, in a Ruby hash, which is basically a dictionary in other langauges.
{
and: [
true,
true
]
}would evaluate to true, while:
{
and: [
false,
true
]
}would evaluate to false
Initially, I wanted to solve this with a reduce block, because items
are in an array and reduce would be perfect as the key for the entry
is the operand (and is only shown, but there will be a need to
consider other logic operators such as or.)
def evaluate_hash(hash)
arg = hash.keys.first
hash[arg].reduce(:arg.to_sym)
endBut this doesn’t work as Array does not accept: :and as an argument,
even though true and false evaluates properly. Hmm…
Quick Solution
Here is a solution I worked out quickly using reduce:
def evaluate_hash(input)
key = input.keys.first
input[key].reduce do |acc, item|
acc && item
end
end
endWhich is a decent solution, but adding in new logic operators start to get pretty messy…
I have to ask, can I do better?
Going Back
I went back and realized approaching the problem using reduce.
The reduce function requires a symbol to be passed to it. So, this approach can work:
describe 'reduce symbol' do
it 'can work with just & symbol' do
test_and = [true, true].reduce(:&)
expect(test_and).to be true
test_and = [false, true].reduce(:&)
expect(test_and).to be false
end
endWhich does exactly what I want.
Designing Elegantly
The shape of the solution I want is along the lines of:
def evaluate_hash(input)
operand = input.keys.first
input.send(operand)
endThis is two lines code, but it’s really clean and basically puts all
the correctness requirements on input.
If the operand was andy instead of and, that’s an input problem,
not implementation problem. Guard clauses can also handle bad inputs.
But, for every new operand added, like xor, this design would require an add another message to the class, ideally not changing this function’s implementation.
Alternative to and?
As reduce requires a symbol and because does not take and:
[true, true].reduce(:and)Returns: NoMethodError: undefined method and' for true:TrueClass
So, is there a better approach?
.all?
Ruby has an Array .all? operator, that is functionally equivalent to
reduce(:&). The nice thing about .all?, it works directly on
Arrays:
describe '.all?' do
it 'can work with .all?' do
test_all = [true, true].all?
expect(test_all).to be true
test_all = [true, false].all?
expect(test_all).to be false
end
end.send(:all?)
The cooler thing, every Ruby object responds to .send, that is
another way to sending the message directly to the object.
describe 'send equivalents' do
it 'can work with .send' do
test_send = [true, true].send(:all?)
expect(test_send).to be true
test_send = [true, false].send(:all?)
expect(test_send).to be false
end
endThis is getting nicer, but the message is: :all?, which itself is
hard coded. To get this to be a bit more flexible, we can use:
all?.to_sym.
describe 'send to_sym' do
it 'can work with .send and to_sym' do
test_send = [true, true].send('all?'.to_sym)
expect(test_send).to be true
test_send = [true, false].send('all?'.to_sym)
expect(test_send).to be false
end
endWhich is cool, because with to_sym, any variable containing text can
be converted to a symbol, like:
describe 'variable to_sym' do
it 'can work with .send, to_sym, and variable' do
all = 'all?'.to_sym
test_send = [true, true].send(all)
expect(test_send).to be true
test_send = [true, false].send(all)
expect(test_send).to be false
end
endRevising evaluate_hash
So, now the original evaluate_hash function can become:
def evaluate_hash(input)
operand = 'all?'.to_sym if input.keys.first == 'and'
input.send(operand)
endWhich is REALLY nice… but I kind of don’t like the conditional: if
input.keys.first == 'and'. Any new operand supported will require a
change here too.
Also, the fact that there’s a translation from 'and' to .all? kinda
looks funny to me.
Monkey Patching
This is one of the great features of Ruby… it can also be the worst feature of Ruby if used badly (and it has!)
But, to really get a more elegant solution, monkey patching really makes a big difference with very little work:
class Array
def and
self.all?
end
end
def evaluate_hash(input)
operand = input.keys.first.to_sym
input.send(operand)
endThe evaluate_hash function does not need to have a conditional
clause on it. All the supported operands are to be monkey patched,
err, included in the Array class.
Of course, with monkey patching, it’s really important to test AND document the work properly.
Documentation is highly recommended on monkey patching the base classes! The shared knowledge of the base classes, like Array, is too great and it would be too easy for anyone new to the codebase to just go in and remove the patches.
Patching Responsibly
Monkey patching one of the language’s base object types such as Array is also dangerous. The sample code uses Array as it was convenient, but in production code, I would create my own class or sub-class Array so there would not less knowledge and code overlap between the classes.
class MyArray < Array
def and
self.all?
end
end
def evaluate_hash(input)
operand = input.keys.first
new_input_array = MyArray.[](input[operand]).flatten
new_input_array.send(operand.to_sym)
endThis adds a conversion for the input array from the original array class to a custom array class, MyArray.
This might seem like overkill, but it’s definitely a sane way of utilizing all the features of Array for a particular use, without loading up the original Array class.
At the same time, localizing patches to this new class instead of the main class helps everyone working on the code base know where to find changes..
Conclusion
What started as a wrong path taken turned into a lesson diving deep into Ruby.
The final solution is more elegant than the original thanks to monkey patching.
Monkey patching is a powerful tool, but don’t monkey patch base objects. Make a copy/inherit and patch those instead. Future You will appreciate the more sleep. :-)