The Subtle Joys of Generic Methods
When you think Ruby, what is your first association? Oh, it's Rails? Hmm. What is you think Ruby feature? Monkey patching? Yep, me too. Programmers have stronger and more heated opinions about monkey patching than adolescent girls have about glitter. Globally modifying class functionality at runtime? Why yes, that does sound dangerous.
With a little imagination you can imagine a doomsday scenario where two libraries wrestle with each other continually to replace some piece of functionality with their own preferred version, and with each release the library maintainers modify their code to move their library back to the top of heap1.
However, monkey patching does provide a solution an important problem: dynamically adding functionality to a class at run-time. Over in Python land we don't have a great solution to this problem, and we've kind of decided we're okay with that: better not to have a zoo than have the lions occasionally escape and gnaw on small children.
However, there is a great solution to this problem. One that allows dynamically adding functionality to a class--nay, to classes--while respecting namespaces and not mucking up the reasonable assumptions in other peoples' code (primarily the assumption that a function won't, without warning, suddenly begin behaving differently, which is a rather important assumption for writing even moderately deterministic code):
Like many great ideas that have slowly seeped into modern programming languages, this idea comes from Lisp--more specifically, Common Lisp--but this one in particular hasn't yet resurfaced in a mainstream language. The solution? Generic functions (also called multi-methods) like those in CLOS. Let's do a few examples of what Python might be like if it had generic function based OO.
class Person(object):
name = None
title = None
def greet(Person p):
print "Hello %s %s" % (p.name, p.title)
So the key difference is that some parameters are typed with a class. Lets say that anything that isn't explicitly typed can be of any type, so we're using an eclectic mix of strong typing and duck-typing (similar to Objective-C).
The first advantage we get is that we can add functionality to a class
from any module, not just in its class declaration. For example, if
the above code is in a module named PersonModule
, then we could write
this code in another module:
from PersonModule import Person, greet
def farewell(Person p):
print "Goodbye %s %s" % (p.name, p.title)
def greet_goodbye(Person p):
print greet(p) + ", " farewell(p)
p = Person()
p.name = 'Will'
p.title = 'Mr.'
greet(p) # print "Hello Mr. Will"
farewell(p) # print "Goodbye Mr. Will"
So we can add methods to an object declared in a different module, but what if we want to override an object's default behavior in our module? Easy as pie.
from PersonModule import Person, greet_goodbye
def farewell(Person p):
return "k thnx bai %s" % p.name
p = Person()
greet_goodbye(p) # prints "Hello Mr. Will, k thnx bai Will"
So that's kind of cool, right? Sure, I'm pulling this out of thin air and can't even prove to you that what I'm suggesting is possible, but this is how OO works with CLOS. There is a working precedent.
And it's awesome.
The joy of generic functions goes a bit further than this as well, giving us some functionality that feels similar to the type matching found in Erlang or Scala. Consider this code:
class Person:
name = None
class Dog:
name = None
def speak(a):
'If none of the more specific patterns match, falls back here.'
print "This is a %s" % a
def speak(Person p):
print "My name is %s" % p
def speak(Dog d):
print "Woof"
def walk(Person p, Dog d):
print "%s takes %s for a walk" % (p,d)
def walk(Dog d, Person p):
print "%s cannot walk %s" % (d,p)
def walk(Person p, Person p):
print "Um. That's weird."
a = Person()
a.name = "Will"
b = Dog()
b.name = "Leo"
c = Person()
c.name = "Jim"
walk(a,b) # "Will takes Leo for a walk"
walk(b,a) # "Leo cannot walk Will"
walk(a,c) # "Um. That's weird."
For those who have used 'real' pattern matching, this will seem like a very cumbersome syntax, but fortunately generic methods don't just give us pattern matching, along with the heavier syntax comes heavier capabilities:
- Rather than only matching on types, the ability to use classes for pattern matching as well.
(You could also phrase this as, classes are a valid type for matching.)
That includes giving us polymorphism by specializing methods on increasingly
specific classes (
Programmer
instead ofPerson
,PerlProgrammer
instead ofProgrammer
). - Ability to override handling of a specific pattern, without rewriting the rest of the patterns.
That said, it doesn't necessarily allow for all of the functionality of pattern matching, because you can't customize the order in which it will attempt to match the pattern (will always go from more specific to more general in a predictable and consistent order, while pattern matching will let you define any ordering your heart desires)2.
Also, depending on the implementation it might not allow specializing on values (instead of just types), but it would be fairly intuitive to add value-specialization to the syntax:
def add(a,b):
return a + b
def add(a, 0):
return a
That would open up some pretty interesting doors. Doors I would like to walk through. (Actually, looks like those doors are already discussed here, and opened here. Now I just need to start walking. Err, and I'd like the syntax to be native. Maybe a pre-compiler of some sort is in order.)
This sounds stupid, doesn't it? It is, and the tragedy is that something comparable occurs all the time in the worlds of anti-virus and toolbars.↩
Depending on the specific implementation (basically, where one establishes the trade-off between explicitly importing all multi-methods you'll use versus them being implicitly imported when you load a module to save typing), it is possible to create a predictable system for multiple inheritance, although at the expense of many many more imports.↩