|
|
Contents: |
|
|
|
Related content: |
|
|
|
Subscriptions: |
|
|
| How generic types can conquer mischievous mixins
Eric
E. Allen (mailto:eallen@cs.rice.edu?cc=&subject=Java
generics without the pain, Part 4) Ph.D. candidate, Java
programming languages team, Rice University 13 May 2003
Java developer and researcher Eric Allen
concludes his four-part discussion of generic types in JSR-14 and Tiger
by discussing the ramifications of adding support for mixins through
generic types. Share your thoughts on this article with the author and
other readers in the accompanying discussion
forum. (You can also click Discuss at the top or bottom of the
article to access the forum.)
So far in this mini-series discussing generic types in JSR-14 and
Tiger, we've covered:
- Generic types and the upcoming features designed to support
them
- The limitations on primitive types, constrained generics, and
polymorphic methods
- Several limitations imposed in these Java extensions
- How the limitations are necessitated by the implementation strategy
used by the compilers of these extended languages
- The ramifications of adding support for
new operations
on "naked" type parameters in generic types
This month, we'll conclude our discussion of generic types in the Java
language by covering the issues that need to be addressed before you can
handle mixins -- perhaps the most powerful feature that generic
types promise.
Mixins versus
wrapping Mixins are classes parameterized by their parent
class. For example, consider the following generic class, which extends
its own type parameter:
class Scrollable<T> extends T {...}
|
The intention of class Scrollable is that it embeds the
functionality necessary for adding scrollability to a GUI widget. Each
application of this generic class would extend a distinct parent class.
For example, Scrollable<JTextPane> would be a subclass
of JTextPane and Scrollable<JEditorPane>
would be a subclass of JEditorPane . Contrast this way to
embed functions with the current functionality in the Java Swing library
in which a JComponent must be "wrapped" in a
JScrollPane if we want to make it scrollable.
Not only does wrapping require adding forwarding methods to access the
functionality of the wrapped class, it also prevents us from using the
resulting scrollable object in contexts where an instance of the wrapped
object is needed (for instance, we can't pass a JScrollPane
to a method requiring an instance of JTextPane ). By
parameterizing Scrollable by its parent class, we are able to
keep a single point of control for the functionality involved in scrolling
while extending multiple superclasses. In this way, being able to use
mixins gives us back some of the power of multiple inheritance but without
the accompanying pathologies.
In the previous example, we could even put a constraint on a type
parameter to prevent it from being used in inappropriate contexts. For
instance, we might want to constrain the type parameter to be a subclass
of JComponent :
class Scrollable<T extends JComponent> extends T {...}
|
Then only GUI components could be extended by our mixin.
Mixins and generic classes: A
perfect match Often, mixins are added to a language as an
independent language feature, as is done in Jam. But it is appealing,
almost seductive, to incorporate mixins as part of a generic type system.
The reason: both mixins and generic classes can be thought of as
functions mapping existing classes to new classes.
Generic classes can be viewed as functions mapping their arguments to
new instantiations. Mixins can be viewed as functions mapping existing
classes to new subclasses. By incorporating mixins using generic types, we
are able to work around many of the key limitations of other formulations
of mixins.
In the Jam extension to the Java language, the type of the superclass
of a mixin has no name; we simply can't refer to it in the body of the
mixin. This limitation snowballs to include all sorts of other problems.
For example, in Jam, the programmer is not allowed to pass
this as an argument to a method; there is no way to type
check such calls. That limitation is crippling because many of the most
common design patterns rely on being able to pass this as an
argument.
Consider the visitor pattern in which a visitor class is defined with a
for method for each class in a composite hierarchy. Typically
the class being visited includes an accept method that takes
a visitor and calls a method on that visitor, passing in
this . Thus, in Jam, the visitor pattern can't be used with
mixins.
With mixins formulated as generic classes, we always have a handle on
the parent class, the type parameter that the class extends. For example,
we can refer to the parent class of Scrollable as type
T . As a result, there are no fundamental difficulties with
allowing this to be passed as a type argument.
However, there are other significant difficulties with formulating
mixins as generic types. Just to give you a taste of some of the
difficulties that can arise, we'll discuss a few prominent ones and some
potential solutions.
Mixins and type
erasure Before discussing any other problems, we should
point out that, like the feature extensions of generic types discussed
last month, support for mixins can't be added to the Java language using
the simple type erasure strategy used by JSR-14 and Tiger.
To see why, consider what would happen when a class that extended a
type parameter was erased. It would end up extending the bound of
the type parameter! For example, every instantiation of class
Scrollable in the previous example would end up extending
class JComponent . That's clearly not what we want.
To support mixins through generic types, we need to have
run-time representations of the generic type instantiations available.
Fortunately, there are ways of encoding this information that are actually
backward compatible with Tiger. Such a backward-compatible encoding scheme
is the hallmark trait of the NextGen formulation of Generic Java (in the
Resources
section).
Available constructors of the
superclass An immediate and pressing problem that arises as
soon as we want to allow for classes that extend a type parameter is to
decide what super-constructors are we able to call? Recall that every Java
class constructor must call a constructor of the superclass. Normally, the
type checker ensures that these super-constructor calls will succeed by
looking up the superclass and making sure that a matching
super-constructor exists.
But when all we know about our superclass is that it's some
instantiation of a type parameter, we have no idea what constructors will
be available for a given instantiation. Also notice that the type checker
can't even check that every instantiation of a mixin will result in valid
super-constructor calls. The reason: a mixin's parameters may be
instantiated with type parameters bound in some other context.
For example, a generic class JSplitPane<T> may
create an instance of Scrollable<T> . We can't know
whether the super-constructors called in Scrollable<T>
are valid unless we know all the ways in which type parameter
T is instantiated for JSplitPanes . But because
Java coding allows for separate class compilation, we can't know all of
the instantiations of JSplitPane during type-checking.
The various solutions to this problem correspond exactly to the
solutions proposed for checking new expressions on type
parameters we discussed in Part
3 last month, because both super-constructor calls and
new expressions reference the same class constructors of a
given class. Let's review those solutions:
- Require a zeroary constructor for all type parameter instantiations.
- Throw an exception at run time when there is no matching
constructor.
- Include additional annotations on type parameters telling us which
constructors those instantiations must contain.
As in the case of new expressions, the first two solutions
have serious drawbacks. Often, it just doesn't make sense to include a
zeroary constructor in a class definition. Also, it's not ideal to simply
throw an exception when no matching constructor exists. After all, the
whole point of static type checking is to prevent exactly that sort of
exception.
The third solution can be wordy, but it has many advantages. Annotate
type parameters with a set of constructors that all instantiations must
have. These annotations tell us exactly what constructors we can reliably
call on a type parameter. Thus, when a type parameter T is
used as the superclass of a generic class, the annotation on
T tells us exactly what super-constructors we can call. If
T doesn't include an annotation, then the type checker
disallows its use as the superclass.
Accidental method
overriding One really big problem that arises with any
formulation of mixins is that the method names of a particular mixin may
clash with the method names of a potential instantiation of its
superclass. For example, suppose that class Scrollable
contained a method getSize that took no arguments and
returned a Size object that encoded its horizontal and
vertical dimensions. Now let's suppose that class MyTextPane
(a subclass of JComponent ) also included a method
getSize that took no arguments but returned an
int representing the screen area of the object on which it
was called.
The resulting classes are shown as follows: Listing 1. An example of an accidental method
override
class Scrollable<T extends JComponent> extends T {
...
Size getSize() {...}
}
class MyTextPane extends JComponent {
...
int getSize() {...}
}
new Scrollable<MyTextPane>()
|
Then the mixin instantiation Scrollable<MyTextPane>
would contain two methods getSize with identical (empty)
parameter types, but incompatible return types! Because we could not have
expected this problematic override of getSize to be foreseen
by either the programmer of class Scrollable or by the
programmer of MyTextPane (after all, they may not even be on
the same development team), we call it an accidental override.
When mixins are formulated as generic classes, the problem of
accidental overrides is particularly nasty. Because a mixin's parent may
be instantiated with a type parameter, there is no way for the type
checker to determine all cases of accidental method overriding. What's
more, throwing a run-time exception when an accidental override occurs is
not acceptable because there is no way for a client programmer to predict
when such an exception will be thrown. If we want to write reliable
programs, we can't allow for unpredictable errors to occur at run
time.
Another solution would be to simply hide one of these clashing methods
and resolve all matching method calls to refer to the method not hidden.
The problem with this solution is that we'd like a mixin instantiation
such as Scrollable<MyTextPane> to be used in both
contexts in which a Scrollable object is called for and in
contexts in which a MyTextPane object is called for. Hiding
either one of the getSize methods would prevent the use of
Scrollable<MyTextPane>s in both of these contexts.
In the context of mixins outside of generic types, Felleisen, Flatt,
and Krishnamurthi proposed a good solution to this problem at the 1998 ACM
SIGPLAN-SIGACT Symposium on Principles of Programming Languages (see Resources):
to resolve references to clashing methods based on the context in which
the mixin instantiation is used. In this solution, a mixin is associated
with a view that determines which method to call in the case of a name
clash.
In the case of mixins as generic types, we can apply the same solution.
We just have to devise some notion of view that works in the
context of generic types and also allows for backward compatibility with
the JVM. At the Rice JavaPLT labs, we've proposed one such solution in the
paper "A First-Class Approach to Genericity" (see Resources).
With power comes
problems As the examples, problems, and potential solutions
demonstrate, extending generic types in Java programming to include
support for mixins results in a powerful language, but it also introduces
problems to overcome. This is typical of programming-language design: a
desirable feature can be added only by complicating many existing
features. In the world of programming languages, there's no such thing as
a free lunch.
Resources
- Participate in the discussion forum on this
article. (You can also click Discuss at the top or bottom of the
article to access the forum.)
- Read the complete Diagnosing
Java code series by Eric Allen.
- For a detailed analysis of the many problems with extending Java
generics to include mixins, as well as proposed solutions, see "A First-Class
Approach to Genericity" (PDF) by Allen, Bannet, and
Cartwright.
- Be sure to read the transcript of "Classes and
Mixins" by Matthew Flatt, Shriram Krishnamurthi, Matthias Felleisen
from the conference record of POPL 98: The 25TH ACM SIGPLAN-SIGACT
Symposium on Principles of Programming Languages for more details on
their mixin solution.
- Learn how the practice of including extraneous zeroary constructors
can cause problems in the author's April 2002 column, "The
Run-on Initializer bug pattern."
- Get a jump on generics in Java by downloading the JSR-14
prototype compiler; it includes the sources for a prototype compiler
written in the extended language, a JAR file containing the class files
for running and bootstrapping the compiler, and a JAR file containing
stubs for the collection classes.
- You can download the NextGen prototype
compiler right now.
- Check out DrJava, a free Java
IDE that supports interactive evaluation of Java statements and
expressions, and supports generic Java syntax and compilation.
- Follow the discussion of adding generic types to Java by reading the
Java Community Process proposal, JSR-14.
- Keith Turner offers another look at this topic with his article "Catching
more errors at compile time with Generic Java"
(developerWorks, March 2001).
- This paper, "Automatic
Code Generation from Design Patterns" (PDF), from IBM Research,
describes the architecture and implementation of a tool that automates
the implementation of design patterns.
- Find hundreds more Java technology resources on the developerWorks
Java technology zone.
About the
author Eric Allen sports a broad range of hands-on
knowledge of technology and the computer industry. With a B.S. in
computer science and mathematics from Cornell University and an M.S.
in computer science from Rice University, Eric is currently a Ph.D.
candidate in the Java programming languages team at Rice. Eric is a
project manager for and a founding member of the DrJava project, an
open-source Java IDE designed for beginners; he is also the lead
developer of the university's experimental compiler for the NextGen
programming language, an extension of the Java language with added
experimental features. Contact Eric at eallen@cs.rice.edu. |
|