Replacing introspection with Adaptation, Revisited

``Potentially-idempotent adapter functions are a honking great idea - let's do more of those'', to paraphrase the timbot.

-- Alex Martelli, on comp.lang.python

All programs that use GOTO's can be rewritten without GOTOs, using higher-level constructs for control flow like function calls, while loops, and so on. In the same way, type checks - and even interface checks - are not essential in the presence of higher-level control constructs such as adaptation. Just as getting rid of GOTO ``spaghetti code'' helped make programs easier to read and understand, so too can replacing introspection with adaptation.

In section 1.1.3, we listed three common uses for using type or interface checks (e.g. using isinstance()):

By now, you've seen enough uses of the protocols module that it should be apparent that all three of the above use cases can - in principle - be handled by adaptation. However, in the course of moving PEAK from using introspection to adaptation, I ran into some use cases that at first seemed very difficult to ``adapt''. However, once I understood how to handle them, I realized that there was a straightforward approach to refactoring any introspection use cases I encountered. Although this approach seems to have more than one step, in reality they are all variations on the same theme: expose the hidden interface, then adapt to it. Here's how you do it:

  1. First, is this just a case of adapting different types to a common interface? If yes, then just declare the adapters and use adapt() normally. If the interface isn't explicit or documented, make it so.

  2. Is this a case of choosing a behavior, based on the type? If yes, then define the missing interface that you want to adapt to. In other words, code that switches on type to select a behavior, really wants the behavior to be in the other object. So, there is in effect an ``undocumented implicit interface'' that the code is adapting the other object to. Make the interface explicit and documented, move the code into adapters (or into the other classes!), and use adapt().

  3. Is this a case of choosing a behavior or a component based on using interfaces as metadata? If so, this is really a special case of #2. An example of this use case is where Zope X3 provides UI components based on what interfaces an object supports. In this case, the ``undocumented implicit interface'' is the ability to select an appropriate UI component! Or perhaps it's an ability to provide a set of ``tags'' or ``keys'' that can be used to look up UI components or other things. You'll have to decide what the real ``essence'' is. But either way, you make the needed behavior explicit (as an interface), and then use adapt().

Notice that in each case, the code is demonstrably improved. First, there is more documentation of the intended behavior (as opposed to merely the actual behavior, which might be broken). Second, there is greater extensibility, because it isn't necessary to change the code to add more type cases. Third, the code is more readable, because the code's purpose is highlighted, not all the possible variations of its implementation. In the words of Tim Peters, ``Explicit is better than implicit. Simple is better than complex. Sparse is better than dense. Readability counts.''

Now that we've covered how to replace all forms of introspection with adaptation, I'll readily admit that I still write code that does introspection when I'm in ``first draft'' mode! Brevity is the soul of prototyping, and I don't mind banging out a few quick if isinstance(): checks in order to figure out what it is I want the code to do. But then, I refactor, because I want my code to be... adaptable! Chances are good, that you will too.