Recently, I had a problem of how to evaluate
and in a data
Specifically, in a Ruby hash, which is basically a dictionary in other langauges.
would evaluate to
would evaluate to
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
But this doesn’t work as Array does not accept:
:and as an argument,
true and false evaluates properly. Hmm…
Here is a solution I worked out quickly using reduce:
Which is a decent solution, but adding in new logic operators start to get pretty messy…
I have to ask, can I do better?
I went back and realized approaching the problem using
The reduce function requires a symbol to be passed to it. So, this approach can work:
Which does exactly what I want.
The shape of the solution I want is along the lines of:
This is two lines code, but it’s really clean and basically puts all
the correctness requirements on
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.
As reduce requires a symbol and because does not take
NoMethodError: undefined method
and' for true:TrueClass
So, is there a better approach?
Ruby has an Array
.all? operator, that is functionally equivalent to
reduce(:&). The nice thing about
.all?, it works directly on
The cooler thing, every Ruby object responds to
.send, that is
another way to sending the message directly to the object.
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:
Which is cool, because with
to_sym, any variable containing text can
be converted to a symbol, like:
So, now the original
evaluate_hash function can become:
Which is REALLY nice… but I kind of don’t like the conditional:
input.keys.first == 'and'. Any new operand supported will require a
change here too.
Also, the fact that there’s a translation from
looks funny to me.
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:
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
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.
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.
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..
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. :-)