Red Green Repeat Adventures of a Spec Driven Junkie

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)
end

But 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
end

Which 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
end

Which 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)
end

This 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
end

This 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
end

Which 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
end

Revising 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)
end

Which 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)
end

The 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)
end

This 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. :-)