Polymorphism
I’ll never forget when I first discovered that Smalltalk has no special forms for conditionals. Smalltalk has no if
statement. That moment changed the way I think about programming (as happens with any worthwhile language, according to Alan Perlis).
A conditional expression in Smalltalk is just a simple method call on a boolean object.
(x > 3) ifTrue: [y process]
The square brackets form a lambda expression; ifTrue:
is the name of a method. Here is the same structure translated into Python syntax.
(x > 3).ifTrue(lambda: y.process())
That all looks fine, if not particularly inspiring. The real “aha!” comes when you start to ask how such a method could possibly be implemented. If there’s no if
statement, how can a boolean object decide whether or not to run the callback function? It can’t, but it doesn’t need to. Since there are two boolean objects, true
and false
(each of which is the only instance of a distinct class, True
or False
), each one can have its own behavior. So true
’s ifTrue:
method simply runs the callback, and false
’s is even simpler.
True>>ifTrue: consequent
^consequent value
False>>ifTrue: consequent
^nil
Again, here is a (ficticious) Python version of the same structure.
class true(bool):
def ifTrue(self, consequent):
return consequent()
class false(bool):
def ifTrue(self, consequent):
return None
This is the definition of polymorphism – two objects can provide the same interface with different behavior.
If you understand this concept and apply it in your every-day programming, you code will turn out simpler, more concise, and easier to modify or extend. How? Let’s look at a practical example.
Example
Here’s a piece of Ruby code I found recently while browsing the Jekyll source code.
class Hash
# Merges self with another hash, recursively.
#
# This code was lovingly stolen from some random gem:
# http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
#
# Thanks to whoever made it.
def deep_merge(hash)
target = dup
hash.keys.each do |key|
if hash[key].is_a? Hash and self[key].is_a? Hash
target[key] = target[key].deep_merge(hash[key])
next
end
target[key] = hash[key]
end
target
end
end
This code defines a new binary operation, deep_merge
, on Hash
instances. It is like the existing Hash#merge
except that it is defined recursively, so that when the left- and right-hand entries are both Hash
instances, they are “deeply merged” instead of one replacing the other.
This code is quite useful and simple enough, but it can be improved. I tend to identify things to improve by the smell; though I don’t usually manage to identify smells consciously, that’s how my mind likes to work.
The first smell I notice here is a “copy, modify, replace” pattern common in imperative-style writing. Rather than copying and updating structures in-place, it’s usually clearer and more concise to generate the new structure directly, so let’s try to make this code more stylistically functional. In this case, we can simplify things a lot by writing deep_merge
in terms of Hash#merge
.
class Hash
def deep_merge(hash)
merge(hash) do |key, lhs, rhs|
if lhs.is_a? Hash and rhs.is_a? Hash
lhs.deep_merge(rhs)
else
rhs
end
end
end
end
The second (and more important) smell is use of the is_a?
method. Often, is_a?
can be replaced by polymorphism.
Because deep_merge
is recursive, we can usefully revise our definition of deep_merge
to apply to all objects, not just Hash
instances. When we do so, most applications of deep_merge
are degenerate – if the left-hand side is not a Hash
, the left-hand object is simply replaced by the right-hand object.
class Object
def deep_merge(other)
other
end
end
The remaining cases can now be handled even more simply.
class Hash
def deep_merge(other)
merge(other) do |key, lhs, rhs|
lhs.deep_merge(rhs)
end
rescue TypeError
super(other)
end
end
(I’ve changed the parameter name here from hash
to other
because we are no longer assuming it is a Hash
instance.)
Organizing things this way has benefits beyond clarity. Suppose we want to alter the meaning of deep_merge
so that it concatenates arrays. All we need to do now is override deep_merge
in the Array
class.
class Array
def deep_merge(other)
self + other
rescue TypeError
super(other)
end
end
Or suppose instead that we want the elements of the arrays to be deeply merged.
class Array
def deep_merge(other)
(0...[size, other.size].max).map do |i|
if i < other.size
self[i].deep_merge(other[i])
else
self[i]
end
end
end
end
Though there’s some noise in this code to deal with arrays of uneven lengths, its structure is still straightforward. Further, we didn’t need to touch Hash#deep_merge
to add this feature.