So, now our multimethod decorator produces things which actually behave like a function – it follows sensible scoping rules, and binds properly so it can be used as a method. That shouldn’t be a high bar, but there you go. Now let’s look at how we might use it.
I’m clearly writing this the wrong way round – knowing what solution I want to demonstrate, rather than having a problem which prompted a solution – because the best example I can come up with is a little bit… stretched. We’re talking checking in to a flight on Terry Pratchett’s Discworld.
When tourists, such as Twoflower, travel across continents, they arrive at the checkin desk, present their paperwork, and are provided with more paperwork to take somewhere else so they can then get on the plane. Their luggage is moved to a holding area, before being moved to the hold. The thing is, because their luggage is sentient and ambulatory, it often arrives completely separately from its owner.
Also, because it’s sentient, it tends not to like being locked in the hold throughout the flight, and because it’s ambulatory, it has the capacity to kick the shit out of the straw donkeys whose mating patterns are dependent on the strange migratory symbiotic relationship they’ve formed with airplanes, so it can’t be left to its own devices. Rather, it must be treated like B.A. Baracus from the A-Team (true in many contexts: see also its complete non-reaction to being punched and its utility as a secure storing place for gold and other valuables), and surreptitiously inapacitated for the duration of the flight, only to wake up at the other end none the wiser as to how it got there.
So, let’s model this situation:
@multimethod(Passenger) def process(x): checkPassport(x) provideBoardingCard(x) smilePassivelyAt(x) @multimethod(Luggage) def process(x): offerWarmMilk(x) manhandleOntoConveryorBelt(x)
This is all well and good until you realise, as all airlines have done, that you’re much better off with a model that has different classes of passenger. First class, business class, etc. So what happens when we try to handle this sort of situation:
class FirstClassPassenger(Passenger): class BusinessClassPassenger(Passenger): class ScumClassPassenger(Passenger): twoflower = BusinessClassPassenger() process(twoflower)
It’s obvious what we want here: a method which takes a
Passenger should take anything which is a Passenger, and any subclass of
Passenger is a Passenger.
It’s also fairly apparent that it’s not going to work with our current implementation: we’re doing a lookup based on class, and there isn’t an implementation for
BusinessClassPassenger. There is one for Passenger, but we’re treating class as a key, as a value. That’s clearly not going to work. If we want to respect subclasses, we need to be a bit more sophisticated.
The good news is: this isn’t hard. Instead of looking up in the map using a key of tuple-of-classes, we instead iterate over the entries of the map, checking each of our actual arguments is an instance of the required type (using
isinstance(), which respects subclassing) until we get a match, and then evaluate that implementation. At this point, it might as well not be a map but just a list of pairs of parameter-type-tuples to implementations.
Sure, that means we’re going from a fast constant lookup to an O(n) iteration over all targets in a multimethod, so calling multimethods like this is going to have more overhead. On the other hand, it now works, so that feels like a reasonable tradeoff to me.
And, so, ta-da – now you have a version which behaves as you’d expect it to when you have many implementations of a class.