[pro] Modularity for subclassing in Common Lisp

Daniel Weinreb dlw at itasoftware.com
Wed Dec 8 17:06:25 UTC 2010


Anton,

Thank you very much for your mail.  It prompted me to explain what I'm
thinking of in a much more clear (I hope!!) way.  Here we go:

Anton Vodonosov wrote:
 >  What is the difference
 > for modularity in case of subclassing, from other cases were we apply 
modularity?
 > I can't find the difference.
 >   

Here's the greater idea.

If you have a module, by using CL packages (without any change to CL),
it is possible to have more than one set of exports.  So, you can
offer different interfaces to different callers.  By interfaces in
this context I mean the set of symbols that are exported.

If you offer interface I1 to callers C1, C2, and C2, and interface I2
to callers C4, C5, and C6, then you can be sure that C1, C2, and C3
will keep working as long as you do not make incompatible changes to
interface I1.  Similarly with I2.

Now, moving specifically to OOP:

Traditionally, e.g. in Smalltalk 80, it was a big deal that the caller
of an instance of a class does not know the internals of the class.
It only uses methods, and does not look internally at the instance
variables (slots).

(I can't remember whether ST-80 had the concept of private methods
that are only intended to be used from methods of the class and not
from callers.)

OOP was often compared to, or even conflated with, the concept of
"abstract datatypes" (ADT's), in the sense meant by Barbara Liskov.
In CLU and such languages, similarly there was a sharp distinction
between the advertised interface presented to callers of the ADT
(abstract datatype, corresponding to a class), and whatever was going
on inside it.

This was a modularity boundary that allowed a separation of concerns
between the users of the ADT and the implementation of the ADT.

Recently, this was considered such a big deal that Liskov was awarded
a Turing Award.

ADT's did not, per se, contain the concept of inheritance.
Inheritance was first exposed to the general public with ST-80.

However, there wasn't even a semblance of an effort to provide any
modular separation between a class and its subclasses.  So, whenever
the implementation of a class changed in any way at all, you risked
breaking subclasses.  This was particularly a problem when library 1,
under control of group-of-people-1, provided a class, and library-2
subclassed it.  If a new release of library-1 came out, there was in
general no way for g-o-p-1 to find everybody who had subclassed their
classes; if the lib-2 people installed the new release of lib-1, they
had no idea whether anything would work.

Now, you could just say that there should be internals and externals,
and the externals exposed to the callers and the externals exposed to
the subclassers are exactly the same set.

This does not work for most non-trivial cases.  I will assume that you
know why, since I can't explain it briefly.

So, what I am proposing is to be explicit about what is being exposed
to the subclassers, so that if the base class is changed in a new
release but the subclasser interface is compatible, the lib-2 people
can rest assured that their library will continue to work.  (Unless
there are bugs, of course.)

In C++ and Java, this isn't done with Lisp packages, of course.  The
"public" members are the interface to the caller, and the union of the
"public" and "protected" members are the interface provided to the
subclassers.

Whether the subclassers should be given permission to override the
public things, in all cases, is an interesting but separate
discussion.  With Lisp packages, you can control this any way you
want.

 >
 >
 > We may note that "protected" is actually public, in the sense
 > that when we specify something as "protected", we specify
 > that the library has external clients which can rely on this protected
 > method or field.

In that sense, yes, absolutely.

 >  Anyone may inherit from our public class, and
 > access the "protected" members, therefore these members are
 > available to public. If we change the protected member, we affect
 > the library clients.
 >
   
What I'm doing, however, is to distinguish between two sets of
"public", as explained above.  The callers, which would be a "big
public" (lots of callers) must cope with incompatible changes
relatively less often, whereas the "small public" (those who subclass)
must cope relatively more often since more is revealed.

 > This drives me to a conclusion, that a single language feature is 
sufficient
 > which allows to separate a protocol for interaction with a module
 > from the module part we expect to change without affecting clients.
 >
 
Taking you literally, we do not need a new CL feature.  But
translating what you're saying into the terms I am using, you're
saying that we need only one protocol, whereas I am saying that it's
better to have two.  There is no "language feature" at the CL level,
but there is a practice, or a "design pattern", that I am
recommending.  Having :documentation in the defpackage to explain
what's going on, especially who is intended to use this package, would
be very helpful. Design patterns are essentially a way to extend a
language (in a nutshell).

 > I made observation on many java systems, that addition to packages
 > the public/protected/private for classes has bad influence on the
 > system modularity.
 >
 > Many java programmers tend to hide attributes of every single class
 > behind get/set accessors, even for simple classes which just hold
 > values passed between methods and do not contain any logic; to
 > create hierarchies of interface/abstract class/concrete for various
 > minor classes, employing protected methods.
 >   

There are at least two reasons to put in such methods.

First, changing the implementation while keeping compatible behavior
for the public methods.

Imagine that you have a class called Point representing a point in
two-space.  You could use Cartesian representation: two data members x
and y, and getX and getY (and, if desired, setX and setY).  These are
the simple getters (and setters) that you mention above.

But what if, for some reason (numerical methods, I don't know) you
decide that internally using a polar representation, with two data
members storing an angle and a distance from the origin, is better.
You can make getX continue to work using the appropriate trigonometry
operations.

Second, these days there are lots of tools that use "dependency
injection" and whatnot that only work if you have such methods.
Extending "ant", using Spring, and so on work this way, so it has
become part of the usual Java design pattern for many classes.  (See
"Java Bean".)

 > But at the same time, people do not care to create structure at larger
 > level, to divide the system into modules. It is not uncommon to see
 > large systems where all these classes, subclasses in all the
 > packages are public: everything is available to everything.
 >   

Sure.

 > Separate access control for classes obscures the idea of modularity
 > (especially in case of such an OO centric language like java, the
 > programmers are mostly concentrated on classes/object, and often
 > don't even think about packages;).
 >   

I don't know what you mean by this.  It depends what you mean by
"access control".  But if you mean limiting the access that callers of
a module have, that is exactly the way modularity is usually realized.



 > Class is too small entity to form a module. A module API (protocol)
 > usually consists of set of classes, methods, factory functions, maybe
 > some global variables.
 >   

Ah, yes.  That's why Common Lisp has packages and Java has namespaces;
to be able to make groups exactly as you are saying.


 > Another thought is that the interface between base class and
 > its subclasses is not a second interface, but a part of the first 
interface.
 > As you said:
 >
 >   
 >> So we could have one package
 >> for the code that calls into our library, which would have the usual
 >> protocol, which we can call the "external protocol".  Then we could
 >> have a *second package*, which presents a second protocol that
 >> subclases would use!
 >>     

The second package IS for the second interface!

 >
 > What would be the name for the second protocol? I.e. what is the
 > interaction the base class and the subclass perform via this protocol?
 >   

I'm not sure what the name would be.  If we were to adopt the practice
I'm advocating, it would indeed be a good idea to have a standard
naming practice.

If you look at some of the major Java libraries, you can see them
dealing with that.  For example, look at JNDI, the standard Java
interface for dealing with any kind of naming service, particularly
hierarchical ones such as file system directories and LDAP servers.

First, there is what they call the "API".  This is used for modules
that want to do things like "look up this name and tell me the
contents" or "for this name, tell me all the children".

Then there is what they call the "SPI", the "Service Provider
Interface".  In the API, the calling module calls JNDI.  For the
"SPI", JNDI calls the service provider module.  A service provider
module is what is often called a "driver".  You'd have one for file
systems, one for LDAP, and so on.  Java comes with some useful ones
like those two, and you can add your own if you want to allow people
who use the JNDI API to access some previously unsupported naming
system.

This is very much like what I'm talking about.  The API and the SPI
are in different Java namespaces (well, as far as I know), and writing
a module that calls the API is extremely different from writing a
service provider.

The SPI might or might not work by overriding classes or adding
:before methods or whatever.

In fact, the whole thing that I am advocating here is NOT ONLY for OOP
and subclassing.  I just presented it that way because that's where I
see the main "pain point".  What REALLY matters is the distinction
between the API and the SPI.

 > As you point, it may be reusing of the implementation (derived streams
 > reuse write-string implementation from the base class).
 >
 > Looks like a thing intended for reusing can rightly be called
 > an "external protocol" too.
 >   

Well, this is something different, I think; a utility class whose
behavior has to be documented.  But this is a side issue and this
email is alarmingly long already. :)

 > Another view on the interaction in subclassing is that the base class 
serves
 > as a template of some functionality, and we can customize its logic by
 > overriding some methods.

In Java, however, the two are separated: there is an Interface that
specifies the contract, and then there might or might not be a
standard abstract base class that provides some helpful stuff.  You
are not required to use the utility abstract base class, although in
practice I think I've always seen it used.

Java allows you to skip having the Interface, which is too bad from
the point of view of clean and simple language semantics, and for
clarity of code.  It's a lot like CLOS's not requiring defgeneric, and
probably for the same reason; to let you be lazy or more succinct.
But I think it's less clear.  I try to always use explicit
defgenerics.  In our own code here, we particularly stress using
defgenerics for functions that are felt to be exposed to other
modules.  (I say "felt" because our code does not isolate modules as
well as it ought to.)


 > The output stream example may be viewed
 > in this perspective: we plug-in our write-character implementation into
 > the functionality implemented by the base class in order to customise it
 > to work with different character source.
 >   

Yes.

 > The means to parametrize/customize the protocol behavior can also
 > be considered as a part of the protocol. In simple case it's method 
parameters,
 > in more complex case we provide, for example, custom strategies, and 
can do it
 > by defining a method for the sublcass.
 >   

OK.

 > Therefore it looks to me that in many cases we deal with a single 
interface,
 > not two distinct ones.
 >   

Well, I think I've explained my point about that above.

 > Sometimes a module can interact with the outside world via several 
protocols, but
 > it not necessary relates to sublcassing, and it's not necessary that 
both (all)
 > the protocols are defined by this module.
 >   

Yes, indeed!  That goes beyond the scope of what I've said so far.
You can indeed have more than one external API, regardless of what you
do with SPI's.

I believe Ada lets you do all of this. More about Ada and the
experience with compatible upgrades, below.

 >   
 >> Now, there's a great thing about CL packages: you can have more than
 >> one defpackage for the *same* symbols. \
 >>     

Exactly.  You can have more than one API, you can have a separate SPI,
you can have multiple SPI's and so on.  This is crucial to everything
I am proposing.  (By "proposing" I do not mean to claim that what I'm
saying is novel and original.  It may well have been done before.  I'm
not trying to take credit for anything here.)

 >
 > It's a great feature. I think you mean something like when we want to 
define one  
 > protocol as an extension of another protocol. Although in many cases 
I suppose
 > it's possible instead of extending interface to have a separate 
interfaces. I.e.
 > if one API is (func1, func2, func3), and another API is (finc1, 
func2, func3, extra-func),
 > we could have the second API just as (extra-func).
 >   

Exactly.

 > A good example is needed. Maybe something like database connection 
API, where implementation
 > for particular database is provided by a pluggable "driver"; or maybe 
swank, and swank
 > backends for different lisps; or something simpler.
 >   

Right, exactly, as I discussed above.

For your reading pleasure, here's is some stuff about Ada.  It's from
Tucker Taft, one of the world's foremost experts on Ada (no kidding).
Note that Ada 95 is a later version of Ada, with improvements to the
original Ada definition.  Tucker was very heavily involved with the
definition of Ada 95, for years.

The point he makes in the first paragraph corresponds to what I've
been saying: the fact that "print" calls "to_string" is NOT apparent
to the caller, but IS apparent to anyone writing a subclass.

----------------------------------------

Here is the mail from Tucker Taft.  I have added a few comments in
brackets [like this].

Hi Dan,

Ada 95 avoids some of the heartache associated with OOP subclassing.
First a little background...

I remember going to an OOPSLA conference when we were just starting
the work on Ada 95, and the panel was bemoaning the maintenance
headaches they were facing.  Someone like Microsoft would try to
release a new version of some object-oriented library, and the users
would scream bloody murder about incompatibilities.  Microsoft would claim
that all of the classes still had the same interface, and they just fiddled
with the implementation.  The problem was that in the implementation,
there were calls from one operation to another, and by default in C++
(and Java and almost all OOP languages), those inter-operation calls
would "redispatch" based on the run-time type of the operand.
In C++ you can prevent this by using "class::operation", but that is
more work and almost no one bothers.

[In other words, Microsoft provided compatibility for the "caller"
interface but not the "subclass" interface.  The people screaming were
the subclassers, not the callers.]

So now what happens when you release a new version of the class library?
Well for performance (or other reasons), some operations rather than
calling other operations, now implement their functionality directly.
A perfect example is a "print" operation and a "to_string" operation.
It would make sense for the "print" operation to call the "to_string"
operation and take the result and send it to the output device.  But it
might be more efficient to put the result directly to the output device
one character at a time, and never bother creating a string.
But what happens if someone had already "subclassed" your class
and taken advantage of the fact that "print" called "to_string"?  They
quite likely would inherit the "print" operation as is, and only bother
to re-implement "to_string".  Now you release your new higher
performance version with its great, more efficient "print" routine
and bang, the subclassers code no longer works as expected
(the inherited print routine no longer redispatches, so it only
prints the contents of the parent class).

[What I'm proposing is that the fact that print calls to_string is
documented.  If a new release changes that, and says that print might
or might not call to_string, then the subclassers have to address
that.  They would still be angry because they have to do extra work
just because of Microsoft, but they would KNOW what work they need to
do.  (Assuming Microsoft documented everything properly and made clear
the protocol (interface) that the subclassers see.]

So how did Ada 95 address this?  It was pretty simple.  Basically,
when one operation of a class calls another, the default is
a "static" binding -- there is no redispatch.  You can explicitly
force a redispatch, but that is more work, and presumably you
only do that if you really want that.  And if you are a good doobie,
you will document that as part of the semantics of your operation.

Hence, in our earlier example, if you want "print" to work by
redispatching to "to_string", then you work a little harder to
force that, and you document that as part of the semantics of
the "print" operation.  If in a later version of the class library
you want to provide a more efficient "print" operation, well
clearly that has different semantics, and deserves a new
name like "direct_print" or equivalent.

[I am 95% sure that what he's saying here, using different jargon than
I do, is the same as what I am proposing.]

The net effect is that a subclasser can treat operations of a class in 
Ada 95
as "black boxes," in that you don't care whether one operation
calls another to gets its job done, so long as the programmer
didn't explicitly decide to "redispatch."  If they did redispatch,
then presumably they documented that fact, and you can safely
take advantage of it.  If they didn't redispatch, then for all of
the operations, "what you see is what you get," so if you
choose to inherit some and override others, you won't get
any nasty surprises when a new release of the class library
comes out.

I hope some of this makes sense!

[In exchange for Tucker's having provided us the service of sharing
his experience above, I am going to pass on his solicitation for
people to comment on his new work.  If you don't want to read that,
you can safely stop here.  But I think it's cool stuff.]


By the way, some of these same issues came up in the work I have
been doing designing "ParaSail" -- Parallel Specification and Implementation
Language.  I would love to come by some day and give you guys a
short talk on ParaSail.  It has some interesting stuff in it.  I started
a "blog" a year or so ago about the design process, and have started
giving some talks in the past few months at conferences and to some
local companies.  Here is the blog:

   http://parasail-programming-language.blogspot.com

If you are interested in seeing some slides from a talk I gave about it,
see this page on our website:

   http://www.sofcheck.com/news/sate_2010.html

The "blog" entry that most directly relates to this issue is:

  
http://parasail-programming-language.blogspot.com/2009/09/parasail-extension-inheritance-and.html

and here is the relevant paragraph:

One important thing to note about how ParaSail implementation
inheritance works.

It is a completely black box.  Each implicitly provided inherited
operation calls the corresponding operation of the underlying
interface, passing it the underlying objects of any operands of the
type being defined.  The underlying interface operation operates only
on the underlying object(s), having no knowledge that it was called
"on behalf" of some extended class.

If the underlying operation calls other operations, they too are
operating only on the underlying object(s).  There is no "redispatch"
on these internal calls, so the extended class can treat these
underlying operations as black boxes, and not worry that if it
explicitly defines some operations while inheriting others, that that
might somehow interact badly with how the underlying operations are
implemented.

Take care,
-Tuck





More information about the pro mailing list