Classes Prototypes Comparison

Just a bit more info comparing PrototypeBasedProgramming to typical class-based ObjectOrientedProgrammingLanguages:

Rather than defining a "class" or a "prototype" from existing languages (and risking a debate over that), some basic object theory will be presented, and the notions of both a class and a prototype will be generated from that. These notions may not agree with what language X does; however it should probably be close to all of 'em - close enough for industrial work. :) If it ain't obvious, this isn't intended to be high scholarship. If you seen any errors, please fix them, but remember to JustCorrectDontPoint.

Anyway, onwards...

Our model

The model of objects we will use comes from LucaCardelli and MartinAbadi?'s A TheoryOfObjects - a good book on the subject (though it assumes quite a bit of familiarity with the LambdaCalculus and CategoryTheory). In the book, an object is defined as a mutable unordered record of functions. Each function has with it an associated type, which we can write as follows:

 (T_0, T_1, ... T_n-1) -> R

where T_0 through T_n-1 are the types of the input parameters, R is the type of the output parameter (we could view this as a classical function where there is only one input argument, which we are making into a tuple to handle multiple-argument functions.Also note that an empty tuple is a possibility).

Note that these functions are (as far as we are concerned) pure, there is no hidden reference to the object (no implicit "this"/"self" pointer). If we need one of those, we have to include it explicitly. Also note that attributes (data members) need no special handling; these can be modelled as functions which ignore their argument and return the attribute value. Finally, note that all functions in this model are replaceable - it is a valid operation on the object to replace a function with another with equivalent type, or to add a function. (Exceptions, including unimplemented functions, can be handled by returning BottomType).

Note that modelling an object as a record of functions is only one possibility; it could instead by modelled by a single overloaded function which includes the attribute name as an argument. The type of an object then becomes an intersection of the individual function types, rather than a record type. This is arguably a more natural approach for prototype-based and actor-based languages, and in some respects it is simpler because it is not necessary for the function to be replaceable. (There is no loss of expressiveness because the environment of the function closure is still mutable, and this can be used to achieve the same effect as replacement.)

[As a language developer, I find excluding a data definition as separate from a function definition in a class, to be very poor. How do you "store" a value to a function? It is obvious how you would store data to a variable that has a physical location in memory but for a function? What about the extra overhead of calling a function versus just accessing the data? To have a function that has a "type" implies that you have types of data so why not just specify that?] -- DavidClarkd

Subtyping

One point often made in OO literature is the distinction between subtyping and subclassing. Subtyping is a relationship between two objects - one object A is a subtype of another object B if certain predicates hold. Subclassing is a mechanism for achieving subtyping.

In our model, an object A is a subtype of another object B (A <= B) IfAndOnlyIf for each item in A there is a corresponding item in B with the same name and type. (Note that CoVariance/ContraVariance do not yet apply, as we are allowing functions in an object to be replaced with other functions). A may have additional members, in which case it is a strict subtype (A < B), or it may have the same structure as B in which case they are equivalent (A = B). Note that both the subtype and equivalence relationships are transitive; furthermore note that A <= B && B <= A implies that A = B.

This notion of subtyping is called structural subtyping; it is supported by some functional languages such as HaskellLanguage and dynamically-typed OO languages such as SmalltalkLanguage. Many OO languages, including CeePlusPlus and JavaLanguage use a weaker notion called nominal subtyping, where annotations in the code are required to declare a subtype relationship (the implementations of these languages ensure that a structural subtyping relationship exists between classes declared to have a subtype relationship). It should be noted that many consider nominal typing/subtyping to have its own benefits, as objects that are structurally similar may in fact represent different abstractions; the concept of nominal typing prevents a ComplexNumber from being passed off as a XyCoordinate?; though both are composed of a pair of numbers. However, the same effect can be achieved using structural subtyping by including a uniquely-named attribute to represent properties of the abstraction not expressed by the other attribute types -- that is, structural subtyping can easily simulate nominal subtyping, but not vice-versa.

Immutable members

Before we continue, a useful thing to do is to consider that some members of an object may change over its lifetime; others may stay the same. If we can declare certain elements of an object as immutable, this gives us several advantages:

What do "immutable" or "read only" variables have to do with "Classes" or "Prototypes"? -- DavidClarkd

Classes

So far, we have dealt with types only as abstract concepts. However, in practical OO code, several patterns emerge:

Given the above, an obvious optimization is the creation of entities which represent shared state between similar objects - classes. The "behavior functions" (methods) of objects were moved out of the objects themselves, and placed in separate entities. In some languages, these entities were made full-fledged objects themselves; in others they were hidden from the programmer and given obscure names such as "vtables". With the addition of classes, a few other benefits arose:

Classes also make nominal typing possible - two objects are nominally equivalent if they have the same class.

As pointed out above, uniquely named attributes can simulate nominal typing without using classes (this effectively substitutes the attribute name for the class name).

While it wasn't necessary to do so (as Cardelli and Adabi point out), functions (other than the zero-argument type that we use to model data members) were banished from objects completely in most class-based OO languages. Most nowadays have ways around this via use of AbstractionInversions like FunctorObjects (which make a function look like data suitable for stuffing in an object's data field); in addition C++ still allows function pointers to be non-static members of an object.

FunctorObjects would be an AbstractionInversion only if they were used in a language where functions are provided as a primitive concept. In languages where objects (or actors) are the primitive concept, FunctorObjects are a perfectly natural approach. There is certainly no need to have both objects and (FirstClass) functions as primitive, since each can easily implement the other. This is not a workaround; it is just economy of mechanism.

Delegation

With classes, comes a fine example of delegation (one that many people aren't even aware of). When you invoke a function on an object; the call is really delegated to the class of the object (where the reference to the function really is). If you type the following in C++:

 foo->bar(1,2,3);

The compiler turns this into something that looks like this (in C)

 foo->_vtable_->bar(foo,1,2,3);

Obviously a very limited form of delegation, but that's what it is... :)

Subclassing/inheritance

With classes came the notion of subclassing. Rather than require subtype definitions to match completely the set of members in a base class (and have to handle errors if they don't), OO languages made it easy - if you define one type to be a subtype of another, you get all of its members by default (including implementations). Furthermore, the corresponding class gets all the members of the base class. The subclass is allowed to redefine any of these that it wishes.

Another popular name for subclassing is inheritance; though arguably the term inheritance is more generic. Subclassing involves inheriting both interface (required for the subtype relationship to be valid) and implementation. You could, of course, have interface inheritance without implementation inheritance or the other way around (though the latter type of inheritance typically breaks the subtype relationship). Most class-based OO languages make you take the whole enchilada.

Jettisoning the class - SelfLanguage

Some programmers started to feel that the restrictions imposed by language implementations of classes were a BadThing. Returning to the fundamental object model introduced in A TheoryOfObjects, the class was axed in SelfLanguage. The language, based on Smalltalk, was started by DavidUngar and RandallSmith at XeroxParc (see http://www.natbat.co.uk/self/selfHistory.php) and announced in 1987. The project later moved to SunMicrosystems, and the Self team was soon redirected towards working on Java. SelfLanguage languished for quite a few years (though Ungar is still working on it, and Sun seems to have cast it loose). SelfLanguage got rid of classes by replacing each of their functions with a completely different mechanism. So classes as object factories got replaced by copying of arbitrary objects, classes as inheritance got replaced with a completely general delegation mechanism, and classes as structural blueprints got replaced by maps. In part, this was necessary by the elimination of any distinction between data and code in Self; an objects' "slots" can hold either data or code, and calling the slot performs the appropriate function.

(AnswerMe: How does CommonLispObjectSystem, which also uses "slots", compare to this?) CLOS doesn't have the concept of "member functions". Polymorphic (aka generic) functions are defined separately from types.

Two other significant prototype-based languages followed - NewtonScript and JavaScript. The former has largely died out with the death of the AppleNewton (although the spirit lives on in IoLanguage); the latter is still cluttering people's desktops with annoying advertisements for Viagra as we speak.

Whoops, structure isn't that bad - the birth of the prototype

While classes as defined in traditional OO languages were limiting, it soon became apparent that the structure they provided is useful. Thus, the concept of a "prototype" is born. A prototype is similar to a class in that objects typically contain references to a prototype object, and can delegate operations to it. Unlike classes; objects in PrototypeBasedLanguages can override the functions defined in prototypes with their own implementations (doing this in a class-based language requires subclassing; though most allow anonymous classes to make this less painful). Prototypes fill other roles of classes - providing a hook for type queries, a classification system, and a way to create new objects. Many PrototypeBasedLanguages, such as JavaScript, even allow addition and deletion of class members at runtime (this is possible in a dynamically typed language; not in a pure statically-typed language).

One other feature that is prominent in PrototypeBasedLanguages is delegation. Rather than making the relationship between object and class implicit and ad-hoc; the delegation relationship is made explicit and more generally available. Some feel that delegation can replace inheritance completely; that is discussed on WhatIsDelegation and other pages.

Interestingly enough, TheoryOfObjects theorizes a statically-typed PrototypeBasedLanguage. All PrototypeBasedLanguages in widespread use are dynamically-typed.

So, classes or prototypes?

The above is history and theory, and I hope that it's somewhat close to accurate. Now for some editorializing.

The OO world seems to be witnessing a dichotomy between class-based and prototype-based languages (one of many dichotomies out there, it seems). This one generates less heat than StaticVsDynamicTyping (not to mention the language-X-vs-language-Y HolyWars), but it exists. To hear some tell it, the two ways of organizing objects are incompatible - you have classes, you have prototypes, but you got to choose.

Hogwash, I say.

Both methods of organizing an OO system or language have the same theoretical underpinnings. The biggest difference between the two (excluding things that are really orthogonal, such as JavaScript's ability to add/delete object fields on the fly) is that with classes, method implementations are immutable and can only be changed by subclassing; with prototypes they are all assumed to be mutable (and thus a bit more expensive to implement). C++ and Java both have good solutions at their fingertips with how they treat data elements. (This applies to some other OO languages as well).

In C++, a data element can be declared const, meaning that it can only be set by the constructor - once set it cannot be altered over the life of the object. (You can try and cheat with a const_cast, but if the results are UndefinedBehavior; you're on your own). JavaLanguage provides a similar capability with final members - once they are set in the constructor, they cannot be changed (Java doesn't allow casting away final, so things declared final really are). [Nitpick: the null value that a final field has before the object is fully initialized is sometimes observable.]

This dichotomy can also be applied to methods. C++ supports const methods (and Java final methods), but both keywords mean something else when applied to a method (in C++, a const method is one which doesn't alter the object through the implicit this pointer; a final method in Java is one which cannot be overridden by a subclass). However, were either language to support (cleanly) methods whose implementation (though not signature) could be changed at runtime they would have much of the benefits of prototypes.

C++, as mentioned above, can fake it with function pointers or FunctorObjects (however the implicit passing of this does not occur; you have to pass in this explicitly). Many UI frameworks for C++ (Qt, for example) use a "slot" mechanism to handle the PublishSubscribeModel. Java can kindasorta fake it with pseudo-functors (objects with a single method with a well-known name like "eval"); the Pizza dialect of Java supports true first-class functions and thus can act like C++ in this regard.

But there is no reason that a language cannot have the best of both worlds. (The same probably applies to many other HolyWars, if people would take off their blinders.)

Unfortunately, there is no advantage to classes. There is no "best of both worlds" since prototypes subsume classes completely. The reverse is not true since class-based languages limit delegation to the inheritance hierarchy. Which is what a class-based language is by definition.

What can you do with true delegation that you can't do with inheritance? Well, you can implement PatternMatching very easily. You can also junk the idea of PredicateClasses. But that's trivial. The real power of delegation comes in when creating wrappers.

With delegation, 'self' always refers to the original object that received the message. In a prototype language that means the wrapper. So if a method in the wrapped object returns self to the sender, then that's the wrapper it returns. OTOH, in a class-based language, self would be the wrapped object itself.

Implementation in Self

A quick look at how prototypes are implemented in Self will, I think, clarify some of this.

All Self objects include a pointer to a structure which is very like a C++ vtable. The Self team calls them "maps". The map describes the object's layout and contains pointers to implementations for methods. When a Self object is modified by creating a new slot or method, it copies the map, adds the new slot, and updates the object to point to the new map. Any other objects which use the old map will continue to use the old map. This design allows objects with identical structure to share the same map. So, in a roundabout way, Self has something resembling classes hidden in the implementation and adding a new method to an object automatically creates a new class. But this view is moronic because it would mean that an object can dynamically change its class at runtime, which is something quite a bit beyond the usual conception of class. And of course, the importance of burying an annoying language "feature" within its implementation can never be underestimated. For instance, dealing with file I/O versus TransparentPersistence.

It is explained in detail in the paper AnEfficientImplementationOfSelf by Chambers, Ungar, and Lee (http://citeseer.ist.psu.edu/14611.html).

This is virtually the same trick (hack?) that Digitalk put into their Smalltalk just before they got absorbed by ParcPlace. At the time, I think they called it "Instance-specific behavior". I think one way to explore the relationship between classes and prototypes might be to ask "What is the difference between a prototype and a class with one instance?"

Well, in some languages (Smalltalk, Ruby, Python, et al) classes are objects. In other languages (C++, Java) classes are nothing at all like objects. I think in languages where classes are objects, the difference between classes and prototypes is barely there. It seems to me that one could easily implement Smalltalk on a Self VM, and likewise, one could easily implement Self on a Smalltalk VM.

True enough. I think its a bit more easy to do Smalltalk on a Self VM, rather than vice-versa, because of the intimate knowledge the Smalltalk VM has about classes and methods. One of the strongest and most interesting arguments in favor of Self, when it first arrived, was the elegance with which it supports class-based programming styles.

Yes, classes are objects; but that doesn't remove the distinction, because objects aren't classes, and classes can't do everything you need to do to use objects.

Indeed there was actually an implementation of Smalltalk on top of Self that at the time actually out-performed most other available Smalltalks (due to sophisticated Self VM trickery which was later adopted into the HotSpot VM I believe).

Are you thinking of PolymorphicInlineCaches?


See LearningFromPrototypes for an idea about giving classes the power of prototypes.

Relevant article: http://web.media.mit.edu/~lieber/Lieberary/OOP/Delegation/Delegation.html (comparing both ways, favors prototypes in conclusion)


CategoryComparisons CategoryPrototypeProgramming


EditText of this page (last edited October 12, 2013) or FindPage with title or text search