September 93 - Reflection
Reflection
Mikel Evins
Many Object-Oriented Dynamic Languages (OODLs) provide support for a facility
called reflection. Reflection in programming is the process of using programming
language operations to examine parts of the program and make decisions about what to
do on the basis of the information gathered. Examples of reflection in programs include
asking an object what its class is, asking a class what its superclasses are, and asking
an object whether it responds to a particular message.
More traditional static languages such as Object Pascal and C++ provide no support for
reflection. Such support would be inconsistent with the minimalist philosophy that the
static languages have regarding the language runtime; reflection requires that support
for certain language-level features be present in the program at runtime, and static
languages avoid mechanisms that require such support because of the overhead
associated with them.
Reflection, therefore, is naturally more commonly supported in programs written in
dynamic languages, and particularly in OODLs. Reflective operations serve natural and
useful purposes in object-oriented programs, and so OODLs such as Smalltalk, CLOS,
and Dylan have always provided reflective operations as part of their standard
semantics. More recently, reflection has become a subject of research as
programming-language theorists try to understand how much and what kind of support
for reflection is practical and useful.
A language that supports reflection provides operations on language elements that
return information useful to a running program. The program can then use the
returned information to control its execution. Information provided by reflective
operations might include the data type of an object, the name, superclasses, and defined
methods of a class, the methods defined as specializations of a generic function, and so
on.
A common use of reflection is in providing programming tools. It should be obvious
that a language in which a program can query runtime data structures about their
structure makes it easier to write tools such as browsers, inspectors, and editors. As
an example, it is quite a bit easier to query an object for its list of superclasses than
to write a tool that searches and maintains a database of header definitions in order to
present an object's superclass list to the programmer. Commonly used OODL
development environments provide tools that make extensive use of reflective
facilities.
We will consider several other examples of the use of reflection in the following
sections.
Examples of Reflection
Reflection is a somewhat abstract idea; it may be hard to see what concrete benefits it
offers a working programmer. Like many unfamiliar features of dynamic
programming languages, reflection is probably best explained by presenting examples.
We'll use the reflective facilities of the languages Smalltalk and Dylan to illustrate the
uses of reflection.
Smalltalk
Smalltalk programmers have several reflective facilities available to them. The
message respondsTo: aSymbol asks an object if it has a method whose name is aSymbol.
The message isMemberOf: aClass asks whether an object is an instance of the specified
class. The message isKindOf: aClass asks the receiver, a class object, whether it is a
subclass of the specified class.
One way to enhance the configurability of an application or system is to package
various parts of its behavior in separate objects. A Smalltalk program can use
Smalltalk's reflective facilities to support a delegation scheme for distributing
func-tionality among objects. When an object receives a request for a service it does
not implement, it can pass the request to another object. This re-routing of requests is
called delegation. Reflective facilities can simplify the implementation of a delegation
scheme by making the calling logic simple. Let's examine several alternatives that
would enable us to provide a flexible software module, and see what advantages
reflection can offer.
Suppose you want to provide a terminal emulation object that offers programmers the
ability to incorporate terminal or message windows into their applications. You know
the general requirements of terminal windows, and in fact they are fairly
stereotypical, but you also know that the range of applications in which programmers
might want to use them is large and so you expect that users of your terminal object
will want to customize its behavior for their own uses.
One solution that is fairly widely used is to heavily parameterize the behavior of the
object. The Macintosh Toolbox and X Windows are both examples of systems in which
the behavior of every major component is parameterized by a large number of
configurable options. Problems with this approach are that, first, it is difficult to
foresee the needs of users well enough to provide adequate parameterization for all
reasonable uses, and, second, that the large number of options tends to make such
components confusing and difficult to use correctly. There are simply so many
parameter values to specify in a well-parameterized example of this kind of
component that it's hard to get all the values right for any particular use.
Another approach that solves many of the problems of the parameterized approach is to
provide an abstract class that encapsulates the most general useful behavior and
permits its users to customize its functionality by writing subclasses. This approach
has great advantages, but it also has some problems. In particular, a user of such a
class may end up overriding enough of the class definition such that he or she has to
rewrite substantial parts of the class just to make minor additions to its behavior. In
addition, this approach some-times leads to unfortunate proliferation of specialized
subclasses, most of which make only minor additions or changes to their ancestors.
Users of static object-oriented frameworks can appreciate how difficult it is to design
sets of classes that interoperate adequately with user-defined subclasses.
In some cases problems like these can be addressed by designing classes to use
delegates that implement additional behavior. In our terminal emulation example, we
could design our object to check for the existence of a delegate and, if it finds it, to call
methods that we provide as hooks for extensibility. For example, our terminal
emulation object could call a post-initialization method to let the delegate know that it
started up and was initialized, character-entered and other editing methods to give the
delegate a chance to preprocess input and to respond to changes in the text buffer, line
condition methods to permit the delegate to handle status changes in the connection to a
communications or interprocess communications stream, and so on.
A scheme like this can make our terminal object very flexible at somewhat the same
cost as the parameterization strategy. We can make it much easier to use, however,
using reflective facilities. If we start up the terminal object without a delegate then it
can omit all its messages to the delegate. Alternatively, we could define all the delegate
methods on the object nil, making them no-ops. The object nil is the value of
uninitialized instance variables, and so method calls with no delegate present would do
nothing.
When a delegate object is present we can use respondsTo: to query it before sending
each delegate message, ensuring that we only send those messages that are implemented
by the delegate. With this approach we enable users of our terminal object to extend
its behavior by implementing only those methods that define the needed behavior; they
don't need to provide definitions for any other methods, because when respondsTo:
returns false the terminal object skips the corresponding message.
In this case we have used reflection to extend the capability of a general purpose
terminal object, while keeping its API as simple as possible. Notice that if we use the
delegation strategy along with Smalltalk's reflective facilities we can ensure that the
API is as simple as a terminal object with no extensibility in the case where we want
to use it just as it is. In the cases where we want to extend the behavior of the object
the API only becomes as complex as we need for the behavior we want to modify. We
never need to see any part of the API except what we want to use, which is a
considerably simpler approach than that of the parameterized object, and even
simpler than the abstract superclass strategy, since we can use the terminal class
directly in many cases rather than needing to define a concrete subclass. Note that the
delegation approach also permits delegates to centralize related behaviors that are used
by several objects in an application; many related objects that need to provide
extensibility can share a single delegate object at runtime.
Dylan and CLOS
Dylan, a new OODL, uses an object model that is more like that of the Common Lisp
Object System (CLOS) than Smalltalk. In the Dylan and CLOS object systems, classes
may inherit from several ancestors and may select methods using several objects at
once to discriminate among alternatives. Instead of message-passing, Dylan and CLOS
rely on generic functions, whose behaviors are determined by the classes of their
parameters. In these languages, then, we use reflective functions, rather than
reflective messages, to gather information about the data and functions in a program.
Dylan includes reflective functions for examining objects, classes, and functions. You
can determine the class of an object by calling object-class, and can test whether an
object is an instance of a particular class using instance?. The function
direct-superclasses returns a sequence of all the classes that the specified class
inherits from directly, and all-superclasses returns a sequence of all classes from
which it inherits.
Because Dylan uses generic functions instead of message-passing, polymorphic
operations are selected by one or more parameters to a function, not by a message
receiver. As a result, there is no precise equivalent in Dylan to the Smalltalk
respondsTo: message. Instead, Dylan provides the function find-method. When you pass
the name of a function and a list of classes to find-method, it returns the method
associated with the function that corresponds to the given list of classes. For example,
the following code returns the Dylan method that adds two instances of
:
(find-method binary+ (list ))
The Dylan function find-method can be used in the same way as the Smalltalk
respondsTo: message; if there is no method that corresponds to a particular list of