Language Type Errors Discussion

For context, see LanguageTypeErrors.


A problem that can be observed in reality is the problem of parallel hierarchies: a vehicle and the corresponding vehicle driver, a car and a car driver, a bus and a bus driver etc. Modelling these classes and the interaction of these classes without covariance is very difficult.

This is a good motivation for covariant arguments, but covariant arguments are no solution. If you "model" this situation (modelling something without keeping in mind what the program should do is imho useless) you say "a bus is a vehicle, a car is a vehicle, a bus driver is a driver, a car driver is a driver" and then make the mistake to also say "any vehicle is driven by any driver", which leads to a car driver driving a bus into a tree. When the police pulls him out of the wrecked bus, he claims "I'm allowed to drive the thing! It's a vehicle, and I'm a driver, dammit!"

The problem is that in reality "Any vehicle is driven by some driver". This fact however is useless when programming: saying that some drivers can drive a given vehicle doesn't tell you if any particular driver can do it, so you could never call the method. Utterly useless when programming. To model this correctly, you need an ExistentialType?. If Eiffel was indeed a language for modelling reality (which is a stupid concept), and BertrandMeyer didn't recognize the need for existential types, that would have been his second major blunder. But I think he's too sane to even attempt such nonsense as an object oriented model of the world.


The paper in ContraVsCoVariance does not describe the Eiffel implementation; instead, it describes a form of MultipleDispatch. (Incidentally, the simplicity of the paper's mechanism only holds in the presence of SingleInheritance; having MultipleInheritance complicates things quite a bit). Covariance in the form of MultipleDispatch is no problem. All of the above discussion implicitly assumes a SingleDispatch OO language. Outside CommonLispObjectSystem and its derivatives (DylanLanguage, CecilLanguage), virtually all production OO languages are SingleDispatch, including CeePlusPlus, ObjectiveCee, JavaLanguage, CsharpLanguage, SmalltalkLanguage, PythonLanguage, RubyLanguage, and, yes, EiffelLanguage.


what's the issue with "catcalls"?

A catcall (cat stands for changing availability of type) is a method call against an object X.M(...), such that the entity X is statically resolved to have the type ClassX and ClassX has a derived class ClassY that overrides M(...) in a manner incompatible with subtyping, e.g., removing it or declaring co-variant parameters. In order to allow X.M(...) the type checker has then to prove that the call is not polymorphic (like the reference X resolves strictly to objects of ClassX and not to any derived class at all) - no polymorphic cat calls.

The problem with the NoPolymorphicCatcalls? rule is this: Suppose you have a working system with a class B with some method m; B may or may not have subclasses. The code contains calls of the form B.m(). Now, introduce a new class D which inherits from B, and overrides m in such a way that D.m doesn't honor the contract of B.m (argument covariance being one way; the subject of this page). In other words, D.m is now a catcall; and B.m a polymorphic catcall (because a variable of type B may be bound to an object of type D). According to the rule, the call B.m() is no longer legal. Unless I'm missing, introduction of a new type into the system breaks the existing code.

And the analysis does appear to require knowledge of the entire system. In particular, it is needed to know which classes are leaf classes and which classes are not; adding new classes to a system can change a leaf to a non-leaf. Perhaps in the applications in which Eiffel has been deployed, this isn't a problem. Bbut in the component-software world where modules from different vendors are composed together, often at the last minute; this is a significant problem.

The frustrating thing is that a simple implicit downcast (a runtime type check) would, IMHO, be a more attractive solution. Not ideal - Meyer seems to think Eiffel suitable for mission critical stuff, and thinks that runtime type queries are incompatible with this (he may be right) - but certainly better than crashing; and for many applications better than having to go modify previously working code to avoid the polymorphic catcall. Interestingly enough, the new "fitted" function he proposes in the paper you cite does precisely this. It takes advantage of return type covariance (which is typesafe) and does a runtime check on an argument of type GENERAL (how Eiffel spells "Object").

I'm sure that BertrandMeyer was aware of the issues before he made the design choice - I still maintain that for most applications it was the wrong choice. (And for mission-critical stuff where runtime type errors - trapped or not - are not acceptable; use of generics - F-bounded polymorphism, in particular - will work, without the problem of new classes breaking existing code.) I assume this is the solution that you refer to that KimBruce? described (an elegant way of doing this that is type-inference decidable is described in FoundationsOfObjectOrientedLanguages?).


Re: "An example where covariance is useful and correct is still missing": it is potentially useful and correct, ceteris paribus, assuming a hypothetical ideal, in precisely the common cases where the mainstream contravariant inheritance fails to allow for an obviously-correct model, for example the infamous "ostrich is a bird but cannot fly" and "ellipse is not a circle, circle is not an ellipse" examples. -- DougMerritt

What's obviously correct there? The CircleAndEllipseProblem precisely shows that there is no subtyping relationship between the two, it only seems to be there because of abuse of the words Circle and Ellipse. Classifying ostrich as bird is a different thing, it breaks the assumption that all birds fly. Either don't assume that in a program or don't label the ostrich as bird. If necessary, refactor the bird class, it's only software after all. When "just modelling the world", you have a problem indeed, but a broken type system won't help you, NonmonotonousReasoning? would. But trust me, you don't want that in a type checker.

Both of you seem to have misinterpreted what I meant by my (ambiguous, I now see) phrase, "fails to allow for an obviously-correct model". I did not mean that there is some existing god-given model that is obviously correct, but which is not allowed. I meant that, of the models that are allowed, none are obviously correct (some are controversially claimed to be correct, which is not the same as being obviously correct to all). Quite a different thing. -- Doug


For a very practical example, it so happens that I even posted code on wiki, long before this discussion, that is the perfect setting for co-variance. See GridLayoutEx. Because JavaTypingWasSimple the protocol between Container, Component (the contained object), LayoutManager - the layout algorithm implementation, and Constraint - the data defining or constraining the position of the component within the container is loosely typed. it could be typed using covariant methods or using ML style parameterized modules (functors). Of course, these technicalities are beside the point of this page. -- Costin

AFAICS the only place where covariance seems to be useful there is the method addLayoutComponent, which could be declared to expect a Constraint instead of an Object. The right thing to do is to remove this method from the interface LayoutManager2. Having it there is a lie anyway, since no LayoutManager can accept any Object as constraint. Of course that would introduce problems for methods which just take a constraint of some sort and pass it on. Clearly, given parametric polymorphism, the problem vanishes. So this example does not apply to Eiffel. In general it is also solved by always grouping a LayoutManager and the appropriate Constraint together or by explicitly using existential (no need for dependent) types.

One of the problems is that in such cases parametric polymorphism is not enough as the actual types may end up being decided at runtime. For example in a truly ComponentBasedDevelopment a form editor may lay out components and serialize the result. Thus some actual types may end up being dependent on values derived from user input (or other external source not available at compile time).

To make a simpler case, imagine an event dispatcher who translates events from some underlying primitive mechanism (X, Windows, a log filter, etc), and calls event handlers for its subscribers. The protocol might be (using Java)

 interface EventDispatcher? {
  subscribe(EventHandler c, EventMask? filter); unsubscribe(EventHandler c) }
 interface EventHandler { onEvent(Event e); }

And now an actual instantiation of an event handler can be:

  ParticularHandler? implements EventHandler{ 

// this does not compile in Java, but illustrates covariant types public onEvent( ParticularEvent? e);

/** * the client of this class has the obligation to use only this constant * to subscribe to an EventDispatcher? */ public static final EventMask? MASK= EventMask?.createMaskFor(ParticularEvent?.class); }
Now the implementation of EventDispatcher? has to dispatch Events polymorphically using the declared onEvent(), while the exact type of a particular subscriber from its generic collection of subscribers is dependent upon runtime values and cannot be abstracted at compile time. The client of this mechanism may prefer to use a covariant and virtual method, because the signature onEvent(ParticularEvent?) communicates most directly the design intention, while he assumes the burden that the correct EventMask? is given to the dispatcher such that the "type unsafe" onEvent() is called at runtime only with values of correct type. A LISP multimethod wouldn't help here either because in case of a value of the wrong type it might select that general case, where what is really wanted would be a safe exceptions indicating the failure to match the expected types. Existential types are not likely to work since the particular type of the event has to be known to both parties (EventDispatcher? and ParticularEventHandler?). Of course, the above design can be materialize in current Java in many ways, but the most straightforward is to do a type cast at the beginning of the method:

  public onEvent( Event e___ ) {
   ParticularEvent? e= (ParticularEvent?) e___;
   // use e from here
   }
This illustrates a more generic problem with type systems: it is guaranteed that no matter how advanced they are, there will always be cases that they cannot handle, I don't know if they proved a theorem similar to the theorem of continuous employment for compiler writers but I wouldn't be surprised if such a theorem can be found for type systems as well. So language designers have to assume trade-offs. -- Costin

But there is a solution, in this instance - use generics: If, rather than having EventDispatcher? and EventHandler you instead have EventDispatcher?<T> and EventHandler<T>, and instantiate these on ParticularEvent?, the compiler will prevent you from ever inserting anything but a ParticularEvent? (or a subtype) into the event mechanism. As many have observed - often times, when you think want covariant inheritance, you really want generics (without any subtyping).

Not at all. One EventDispatcher? "object", has to dispatch to a dynamic number of handlers corresponding to a dynamic set of Event types but all deriving from Event. Now there you go, try one more time.

Of course, there are times where you have to use inheritance and covariance - in which case you must either use a typecast, or run the risk of UndefinedBehavior. -- ScottJohnson

Or have the runtime do the type cast for you as part of dispatch mechanism, in case that the actual method overrides covariantly in the derived class. That would have been the sensible solution that B. Meyer could have adopted.

There is still a way to implement the EventDispatcher? with parametric polymorphism, existential types and without covariance. To do so, the EventMask? has to be a function, not just a predicate. Such a function can return a different type, which would be the type the concrete EventHandler expects. The signature would be something like:

 interface EventDispatcher? {
     subscribe( forall T . EventHandler<T> c, T filter(Event) ) ;
     unsubscribe( forall T . EventHandler<T> c) ; 
 }
It's important that filter can also return a null reference here. Of course I'm severely abusing Java's syntax. If this is to be implemented in Java, the filter function would have to be implemented as an Object and the forall qualification would have to be hidden by combining a EventHandler and a suitable filter in yet another class. The naive type system of Java is really of no help here. It might be far more practical to simply unify EventHandler and filter function, so it would have a completely generic handle() function that also told the caller whether it really handled the event. However, in a language with explicit universal qualification and parametric polymorphism, the above is type safe and works without runtime type checking.

No, I do not think you had all the requirements that I intended and you slightly restricted the problem to make it typable. I do not think that Java extended with existential types would be able to solve it. I do not think it could be programmed to be functionally equivalent and fully statically typed in any of Ocaml, ML, Haskell as defined by the latest Ghc. The Ghc solution would require monads for the setting is imperative and EventDispatcher? is intended to be connected to an I/O source for events. But I'd like to be contradicted nonetheless so I'll try to formulate it more precisely.

To make it more clear: objects of type EventMask? are opaque to EventHandler, the event handler simply declares what he's interested in. If this was ML/Ocaml, EventMask? would reside in the same module with EventDispatcher? not with EventHandler. Furthermore, actual Event objects are instantiated from an external source, and the compiler/linker may not even have access to all Event subtypes when EventDispatcher? is compiled - the function/method selection for handleEvent(e) has to involve runtime polymorphism. Here's more extensive Java code - of course java is more dynamic than ML/OCaml/Haskell because of its reflection facilities:

  public class EventMask? {
     protected abstract boolean accept(Event e);

/** * constructor for the actual hidden type */ public EventMask? createMaskFor(Class evtClass) { // dummy unoptimized implementation can be replaced by // anything else without breaking EventHandler clients return new EventMask?() { boolean accept(Event e) { return evtClass.isInstance(e); }}; } }

public class EventDispatcher? { List<Pair<EventHandler,EventMask?>> subscriber_list;

// ... code for deserialization, maintaining the list of subscribers etc

/** * this method is connected to the input thread and dispatches events to handlers */ public void process(String input) { Event e= (Event ) deserialize(input); foreach << handler, mask in subscriber_list >> { if (mask.accept(e)) handler.onEvent(e); } } }
Now the supplier of a "plugin" package that can even be deployed dynamically, can have the following code.

  ParticularEvent? extends Event { /*...*/ }
  ParticularEventHandler? implements EventHandler {
    static final EventMask? myMask= EventMask?.createMaskFor(ParticularEvent?.class);

// polymorphic covariance is useful here onEvent(ParticularEvent? evt) { }

// activation method to be called by a third party public void doSubcribe() { EventDispatcher?.instance().subscribe(this,myMask); } }


I didn't restrict anything, though I may have overlooked something (which isn't all that hard, longwinded as Java is).

Okay, I'll try to be as precise as possible. For the sake of argument, I'll stick to Glasgow-Haskell. I hope, the syntax is self explanatory enough. We'll need Haskell with explicit universal quantification (or maybe existential types, I'm never really sure which is which), and the Dynamic datatype might also come in handy. Of course, where IO is involved, the code becomes monadic. That doesn't matter much, code in Java is monadic anyway (though Java programmers generally don't want to know about that).

First, have a look at the EventMask? class. This is basically nothing more than a predicate on Events, realized by the accept() member. We make this explicit, EventMask? simply is the predicate:

 type EventMask? = Event -> Bool
After evaluating the predicate and it is true, the Event is question has to be downcast to some ParticularEvent?. If the predicate if false, there's no need for tha cast to be defined at all. So we simply unify the cast and the predicate into the EventMask?, which is now polymorphic in the result type:

 type EventMask? particular_event = Event -> Maybe particular_event
Now for the handler. Each EventHandler basically consists only of the method handle(). We might as well say, it is this function and nothing else. Handling an Event presumably involves IO, so we get something like:

 type Handler particular_event = particular_event -> IO ()
What the EventDispatcher?? Mainly a list of EventMasks? and associated EventHandlers?, plus functions onEvent and doSubscribe. It's a singleton, so I won't bother with an object and just define free functions onEvent and doSubscribe and I'm not concerned where the association list is to be stored. That doesn't matter anyway.

 data Pair = forall p_event . Pair (EventMask? p_event) (Handler p_event)

doSubscribe :: EventMask? p_event -> Handler p_event -> [Pair] -> [Pair] doSubscribe mask handler = (Pair mask handler :)

onEvent :: Event -> [Pair] -> IO () onEvent event = mapM_ try_dispatch where try_dispatch (Pair mask handler) = case mask event of Nothing -> return () Just p_event -> handler p_event
Only one thing is missing: what's an Event? In practice probably a sum type of all the possible ParticularEvents?. Here we're just going for maximum extensibility and wrap a dynamic type. That makes implementing the concrete masks very simple: just extract the dynamic value. (Sorry for possibly mistyped function names or wrong types; I'm not not using the Dynamic stuff often enough.)

 data Event = Event Dynamic

createMaskFor :: EventMask? p_event createMaskFor (Event dyn) = fromDyn dyn
Haskell has no reflection, which makes an equivalent to your createMaskFor impossible. We get something else here: the type of createMaskFor may have to be declared at the point of use and the compiler will put in an appropriately typed fromDyn, which achieves the same effect as the reflection does.

So in conclusion, I think, this is all really equivalent to your Java example, and still well-type even without runtime casts. (fromDyn could be thought of as a downcast or as a case analysis, but types only need a runtime representation within Dynamic.) How to deserialize an Event will be left as an exercise for the interested reader.

Now have a look at the strange types. We have only on data type with an existential type, and the only thing we do with it is to extract both components to immediately compose them. We might as well compose them earlier and don't need existentials any more:

 type GenHandler? = Event -> IO ()

doSubscribe :: EventMask? p_event -> EventHandler p_event -> [GenHandler?] -> [GenHandler?] doSubscribe mask handler = (gen_handler . mask :) where gen_handler Nothing = return () gen_handler (Just p_event) = handler p_event

onEvent :: Event -> [GenHandler?] -> IO () onEvent ev = mapM ($ ev)
E voila, again an equivalent program, no cast, no covariance, no existential types, only parametric polymorphism. (And notice how few monads, how short it is, how readable, how it almost cures cancer and how it attracts chix. But enough of the advertising...)

Granted, I took some shortcuts. There are no more "objects" and there's no inheritance. Encoding objects into Haskell is cumbersome (and mostly unnecessary), encoding advanced types into Java is cumbersome as well and above all purely hypothetical. So I'll leave it as is.

You took quite a lot of shortcuts, which makes it far from being equivalent, but instead typable.

In the end the key that I see in your design is that you made Event dynamic - and now you cannot make Event the sum of all particular event types, because event types can be loaded dynamically into a system - with Java this is trivial. This is a slight of hand maneuver as you observed yourself: we can always add a bottom type, or the other way (the Harper way) we can always encode Scheme in an algebraic data type and a bunch of functions.


Other than particular examples (such as the LayoutManager related contracts) that are quite common in OO practice, it is very intuitive to understand why contra-variance is unnecessary while co-variance is frequent. Often times derived classes are more specialized version of the base classes. Since rarely a class can stand on its own, the collaborators, which often end up as method parameter types, are more specialized as well. It is quite counter-intuitive (and I don't think it has eve been shown in practice) that a special case needs more general collaborators (the contra-variance case).

Of course, this poses exceedingly difficult problem for type theorists to come up with a proper formalism to address this situation, and certainly type theory was not advanced enough at the time that Eiffel was designed. This doesn't excuse B. Meyer for not having inserted a runtime safety check - I frankly don't know what possible argument he has about that.

Specialized methods allow more general parameters. If they instead require more special parameters, they are no longer a specialization of the original method, but something new. It's not all that counter-intuitive.

I believe that this assertion follows the "modelling the world" philosophy that you just criticized. In certain contexts methods with covariant parameters can be viewed as specialization of methods.

Type systems are not a fixed world where only one perspective can be accommodated. It is more like lego pieces: a designer may prioritize this feature versus the other feature, make some designs more cumbersome or leave some features behind to address pragmatic concerns. That's why I found the statement that "TypeTheory concluded that [...]", objectionable. It sounds too platonistic for a branch of mathematics that is constructive par excellence. Type theory doesn't dissect the world of types like real analysis dissects the world of reals. Type theory instead offers either complete formal models (lots of them) or bits and pieces from which language designers (more often than not the same as the type theorists) pick and choose. So type theory never concludes that the world of types has property X, but rather "if you construct a type system having these features and these underlying assumptions the following important theorems (properties) will apply".

The underlying assumption behind a simply typed lambda calculus is that subtype values are substitutable in all contexts (from where contra-variance is derived). A language designer who wants to accommodate co-variance can say, we'll relax that assumption and say: subtype values are substitutable in all contexts except: <case A>, <case B>, and we'll take the following design decision for such exception (ignore them and let the program crash, introduce a runtime check, stick his head in the sane, etc). And this doesn't mean he runs afoul of TypeTheory.

Well, of course you can always "repair" TypeTheory by lifting types (adding a value bottom that represents an error) and simply returning an error in all cases that no longer suit you. (In fact, all type systems have to do that to some extent lest then become undecidable otherwise.) Whether that suits the callers of the method in question is a different thing. I certainly prefer methods that don't blow up when called, and the type checker is there to convince me that they don't. Before I put up with "a subtype is substitutable for a super type except when not", I'd rather ditch the static typing completely. You seem to prefer static typing and get a runtime exception in the exceptional cases. We differ on that, but at least we both don't want to put up with undefined behaviour...

And regarding "modelling the world", I actually prefer functional programming with parametric polymorphism. Subtype relations aren't all that important there. Once your programming language works well without subtyping, you start to realize that there aren't that many subtype relationships out there. So whenever you stumble over this "oh, that's only almost a subtype", it simply is no subtype at all and that's no big loss. Oh, and I look at the code when deciding what's a subtype, not at the things in the BigBlueRoom.


(OffTopic meta discussion below, DeleteWhenCooked)

Just to set the record straight, I have not been participating in these conversations until today's comment. I presume you know that, so I presume your comments are mostly aimed at the others who have been debating you, and you were just using my little comment as a springboard. However, I personally am interested in these technical details. -- Doug


Costin, I gotta ask:

Why on earth are you so worked up about the original content of FamousLanguageTypeErrors? (which, after being refactored away, you've apparently re-introduced and continue to flog at on several pages)? Is BertrandMeyer your uncle or something? It is hardly the most inflammatory thing on Wiki; compared to many of the criticisms to be found here, it's pretty tame. Some of the tone can be softened, certainly - but I'll stick my neck out here, and note that you're frequently far more inflammatory in your dealings with others - and I'm certain numerous others will agree with me on that point - that it's deliciously ironic to see you complaining about chest-thumping criticism written by someone else.

(Plus, given BertrandMeyer's well-documented history of offensive and obnoxious pronouncements - many of which would make even EwDijkstra blush - many of us felt a bit of schadenfreude at seeing his nose rubbed in the dirt. When someone publicly conducts himself like an ass; he deserves to occasionally get treated like one.)

You might also consider that many of us, myself included, don't post anonymously to avoid criticism or responsibility for our words; but instead to donate them to the wiki. EgolessWiki and all that. You seem to regard anonymous posters with contempt (or at least suspicion), but for many people it's the preferred way to post.

-- ScottJohnson

Scott:

I'll believe in EgolessWiki when I'll see one. I, myself, contribute anonymously when I'm presenting matters of fact, stuff that anybody with experience in the field could write equally well or better. When I know there's even the slightest chance to be wrong, have an incomplete perspective on stuff, or present highly personal opinions or value judgements then I know that I should better sign. I appreciate donating knowledge and effort to wiki, but donating material for TitillatingEgo? (schadenfreude and all that) anonymously, I frankly do not appreciate. As for this not being the worst case on wiki, I am perfectly aware of that. Thanks, in part, to benevolent bystanders (PeanutGallery?) like you, we do have worse. I occasionally deal with such things in a non-deterministic order - and I'm not the only one, but I certainly look like an attractive target. -- CostinCozianu

You are reacting far too strongly. ScottJohnson is by far one of the more generally reasonable people here, but you are rejecting his input out of hand, and even claiming "we do have worse" due to people like him, which is really going too far. The wiki is a better place for his contributions, he's not just some random clueless kibitzer! Disagree if you must, but distinguish the clueless from the clueful when you do so, please, with more respect (or at least less disrespect) for the latter kind of people. -- DougMerritt

I stand by everything I wrote about Scott, and I never implied he was a clueless kibitzer. That makes him all the more responsible for the non-sense he signs on. -- Costin

Well, still, you're calling his comments nonsense (which to me sounds like it implies a judgement of "clueless", but I don't want to argue the point if you disagree, although feel free to clarify if it would help promote communication), whereas I think there's reason to recognize this as one of those cases where reasonable people can disagree. Both you and he use personal judgement as to when to sign posts versus when to leave posts anonymous, after all; neither of you is 100% polarized on the issue. So it seems to me that your disagreements on that particular topic are about which situation calls for which. -- DougMerritt

Yes, I'm calling one particular comment nonsense as a matter of logic: the "B. Meyer is an ass" line of reasoning is a non-argument. And as a matter of style, I find it simply in bad taste, especially aggravated by a gratuitous reference to Dijkstra. Maybe Scott was too busy trying to administer a counter punch to yours truly and forgot to double check if what he was trying to communicate and the way it ended up in the form of English text on a wiki page were really the same thing.

I do not think I and Scott disagree on using signatures, most quality contributors follow the guidelines I mentioned, and the guidelines are in themselves quite flexible. I just made it clear that it never crosses my mind to attack anonymous contributors in general, but I do have a problem with folks who deviate from this wiki tradition about when to sign and when not to, especially when they use the cover of anonymity to mount dubious attacks.

And hey, this is wiki, everybody makes mistakes, including possibly me in my response to Scott. He can defend his points, he can delete everything, he can delete parts of it leaving what is relevant. Or leave the whole clutter here. -- Costin

Of course, I didn't say that BertrandMeyer is an ass; I suggested that occasionally he acts like one. To me, there is a difference - just because someone (whether you, me, or Meyer) is a published author and a well-recognized authority on a subject, doesn't give them a license to escape rebuke when their public comments become undiplomatic. At any rate, it's no big deal; I'm prone to act like an ass myself, and may have done so on this page. We all act like asses sometime - and often times the person who acts like an ass is the person who is the last to know about it; that's human nature.

OTOH, I have yet to act like an ass in the published literature - whereas Meyer has published diatribes such as BewareOfCeeHackers, which have earned him well-deserved rebukes from other CS notables. And my reference to Dijkstra isn't without merit. While one must recognize the numerous contributions that Uncle Edsger made to the discipline, one must also remember that Dijkstra was quite prone to publishing statements in the literature which are essentially flames, even if articulately expressed.

Let me give you a few real matters of fact for comparison, just to get you back in touch with reality and with proper English usage vis a vis "matters of fact":


At any rate, I will restate for the record that I still consider Meyer one of the leading authorities on OO; and ObjectOrientedSoftwareConstruction is still, IMHO, the definitive work on the subject. -- ScottJohnson


DecemberZeroFive

CategoryTypeTheory


EditText of this page (last edited August 15, 2011) or FindPage with title or text search