Alternate Object Oriented Programming View

[Please refine this view here, or DiscussAlternateObjectOrientedProgrammingView.]

If you ask most programmers what object-oriented programming is, they usually regurgitate something about polymorphism, encapsulation, inheritance, methods that belong to classes, objects that "receive" method calls or messages, and such.

In reality, most of these features are either irrelevant to ObjectOrientedProgramming, or are the result of unnecessarily crippling it, usually for the sake of maximum performance with minimum effort on the part of the language implementor, or for the sake of rejecting correct code because it exhibits a style that is frowned upon for nontechnical reasons.

In its barest essence, object-oriented programming revolves around one simple idea: that an expression in a program, consisting of a function identifier and one or more arguments, can stand for multiple implementations. When the expression is evaluated, a particular manifestation of that function is chosen based on the combination of some relevant run-time properties of some or all of the arguments. Usually those relevant properties are the types of those argument values, but other properties may also determine the choice. (For example, the CommonLispObjectSystem can specialize a parameter to a particular integer value, or the identity of an object such as a symbol).

Polymorphism

The function which manifests many implementations is called a GenericFunction, and those implementations are called methods. The idea that one syntax can stand for multiple implementations is called PolyMorphism, but not all polymorphism is object-oriented; only that polymorphism which involves evaluation of the properties of arguments of a function call to find a suitable piece of code that handles that combination.

PolyMorphism is also inherent in DynamicTyping without object-orientation; a function that can accept either a string or an integer, and deal with these types properly, is polymorphic. But it's not a generic function in the sense of the term we are using here; it has to contain explicit code to deal with parameters of different types wherever that difference matters. However, this is an important kind of polymorphism nevertheless which combines nicely with object-oriented programming. A programmer may use a regular function at the outset, but later it becomes obvious that it has a "generic nature"; it represents a concept that can work with many types. It may then become convenient to turn it into a generic function. Now the type cases are handled by the method dispatch mechanism, so the methods don't have to contain ad-hoc code for inspecting types and handling the cases.

In a sense, methods are the result of the realization that handling combinations of types with special case code is a programming concern that deserves to be separated. To have support for object oriented programming means that the programmer can declaratively express the type combinations and their relationship to the code that is selected, and the language provides the automatic reasoning which matches a call to a method. Methods are a lot like facts in a logic knowledge base; there is a relationship between unification and the search for a method. Both are a kind of pattern matching activity that works with a database.

IN any case, it's clear that a particular kind of PolyMorphism is the defining concept in ObjectOrientedProgramming.

Classes

Object-oriented programming languages without exception allow the programmer to define her own types. A user-defined type is called a class in OO jargon. Thus generic functions can be specialized to user-defined types in addition to any axiomatic types.

It's hard to achieve ObjectOrientedProgramming without user-defined types. So the capability to define new data types with custom properties should be viewed as a necessary component of ObjectOrientedProgramming. Otherwise generic functions and their specialized methods can only be written over the axiomatic types that are built into the language, such as strings and numbers. That would be a serious limitation.

In PrototypeBasedProgramming, every object defines its own type, which can be used by other objects.

"Classes are types" is a bit vague. A class hierarchy can be thought of a s a mechanism that conveniently combines two essentially orthogonal ideas: Inheritance and subtyping. Classes allow you to coin new types and re-use code all in the same operation. (http://www.cmi.ac.in/~madhavan/courses/pl2005/lecturenotes/lecture-notes/node28.html)

Inheritance.

Usually, user-defined types can be related by inheritance. This means that an object can have more than one type at a time, and these types are arranged in a hierarchy that flows from general to specific. The more specific types, called subtypes, are formed by inheriting the properties of other types, their supertypes. Other terms used are superclass versus subclass, and base class versus derived class.

Inheritance gives programmers a way to reduce the amount of code written by factoring out common elements among types and pushing them to a superclass. For example a bear and elephant share a lot in common; they are both animals with many common features that may differ only in some manifestation details. It may make sense to define an animal type and then create the bear and elephant types based on it, thereby reusing the existing type definition rather than duplicating it. Secondly, inheritance allows a method to be specialized for a supertype parameter. That is, for some generic function, we can write a method that has one or more parameters that are specialized to the animal type. That method will work if the actual arguments for those parameters are bears or elephants. We don't have to write separate methods for that function for bears and elephants, but just one. But we have to be sure that the method really is applicable to to any animal, regardless of its more specific type.

Also, the method selection works such that the most specific method is chosen. If the function is called with an argument of elephant type, and a method exists which specializes the corresponding parameter to animal type, and another method exists which specializes to elephant type, the elephant one will be chosen. In principle, either method would work with the elephant, but the one specialized to the elephant class is more specific to elephants than the one specialized to the animal class. In the case of multiple arguments, there can be ambiguities: one method is a more specific match for one parameter, another method for a different parameter. The system has to have disambiguation rules, or else reject ambiguities. MultipleInheritance also introduces ambiguities: two methods can appear to be equally specific for a parameter because they specialize to a different superclass at the same level in the type hierarchy. Again, this situation is either subject to some disambiguation rules, like left to right precedence among superclasses, or can be identified as an error. Selecting the most specific specializations provides a key flexibility that makes inheritance useful. It allows behaviors that are defined for superclasses, like animal, to be overridden with specialized behaviors for specific types. The programmer who introduces a new, strange kind of animal is not stuck with the behaviors of existing methods over the animal class, and she does not have to rewrite any of those methods either, which would affect all of the other animal subclasses. She can instead specialize these methods, where appropriate, to her new class to capture the calls. In other words, it's not necessarily true that some method that works for animal will work for elephant if an elephant method is available; that more specific method may well exist exactly for the reason that an elephant is different somehow, and so the plain animal method won't work.

In summary, contrary to popular belief, inheritance is not required for object-oriented programming, but it does enhance object-oriented programming. Two types don't have to be related by inheritance in order for the same parameter of a generic function to be specialized to either of them by two separate methods, but it's a great labor-saving and complexity-reducing device to be able to factor out commonality.

That ObjectOrientedProgramming requires inheritance is an extreme view, and so is the view that inheritance is an unnecessary hack that introduces unwanted coupling among classes. Neither viewpoint is quite correct. Though not necessary, inheritance is useful, and its coupling is obviously wanted. That is, it represents some tradeoff: the programmer factored out commonality to simplify the program. Of course the material pushed into supertypes has a big dependency fan-in; that's how it works. Part of the coupling-is-bad argument against inheritance stems from the worship of encapsulation. Inheritance works against encapsulation because it distributes behavior through the hierarchy of method specializations, so that it's not centered around a single class. When common pieces of an elephant and bear are factored to make an animal class, the existing methods that remain specialized continue using the parts of the object that were moved to the superclass.

Encapsulation

Encapsulation is completely irrelevant to ObjectOrientedProgramming. It's essentially a set of scoping rules and access restrictions which are possible when OOP is weakened. Encapsulation says that code and data are packaged together: that methods are associated with a class.

In order for this illusion to work, various restrictions are laid down. Firstly, a method is selected on the leftmost parameter of a function call only. In some syntaxes this is emphasized by syntactic sugar in the form of a special function call notation in which the leftmost parameter appears alone, to the left of the function, rather than enclosed in the parameter list. For example: object.function(arg1, arg2) rather than function(object, arg1, arg2). In some other object systems, a method is viewed as a "message" that is "received" by an object. The syntax may be something like send object message arg1 arg2. There is no real difference between "message" and "function". In any case, when method dispatch is confined to one parameter only, it gives rise to the illusion that it's under the control of the properties of the class of that argument, and consequently that it somehow "belongs" to that class.

Encapsulation does not stop there. Because a method appears to belong to a class, why not enforce it? The class definition syntax can be extended such that a list of methods must be written along with the class definition. Then elsewhere in the program, a method can be written with a leftmost parameter specialized to that class only if it appears in that class. And perhaps, additionally, if its remaining arguments have a signature that is also defined in that class. See BondageAndDisciplineLanguage.

Next, because the leftmost parameter is treated specially, it's possible to avoid giving it a name, and instead introduce the notion of class scope. Over the program statements that make up the definition of the body, it's possible for the compiler to alter its scoping rules so that the properties of the class are visible without any qualification. If the class has a slot called COLOR which represents the color of the object, then in the method, the naked identifier COLOR can be used to access that slot. That the leftmost object must be accessed to get to this value is implicit. Or, in some languages, a special keyword represents the nameless leftmost parameter, such as the keyword self. In any method, self behaves like a variable that is bound to the leftmost parameter, which otherwise has no name. An expression like self.color refers to the color slot of the object to which this method belongs.

Lastly, class scope can be extended with access rules. The method names themselves can be folded into class scope. A class definition can annotate methods and slots with access leaves like private, public or protected. Various rules are introduced regarding which scope can access what. Only a method of a class can call a private method, or see a private variable, and so forth. CeePlusPlus has such access rules, but it is noteworthy that any correct C++ program which uses these access rules can have them removed. The result is also a correct C++ program which does exactly the same thing.

Another variation on this theme is to require methods to be completely written within the body of the class-defining construct, so that they are lexically scoped to the class. Either way, the restriction means that adding new methods means that the class definition itself must be edited. A programmer cannot just write a new generic function and create methods specialized over existing classes; he must edit the class definition.

There are other ways to achieve reasonable encapsulation so that program modules, on a larger scale than individual classes, have well-defined interfaces with public and private parts. One such mechanism is a sane package or NameSpace system. Some discipline is clearly needed on a larger scale, so that one team of programmers can communicate the idea: "please don't make your code dependent on these parts of our component; they are internal and subject to change without notice".

Encapsulation is not only inessential to ObjectOrientedProgramming, but it weakens OOP significantly, and erects barriers that get in the way of programming, period. Encapsulation does not reduce programming errors. Rather, it identifies certain programming situations which are not defects, and declares them to be defects. These are consequently political defects, not technical defects. Programmers who require this type of childish enforcement will find a way to make a mess out of the software in spite of the restrictions.

Message passing

Real message passing takes place when a program is distributed into multiple address spaces, perhaps residing on different machines. The various components contain servers that provide services to each other, giving rise to a distributed application. This is a kind of left-argument-only dispatch: the left argument selects an instance of a server, and the rest of the arguments are packaged into a remote procedure call. Moreover, there is clearly encapsulation because you can't just reach into another address space! The ObjectOrientedProgramming page presents a view of OOP in which each object is essentially viewed as a component in a distributed system; its own miniature kernel with its own set of system calls and its own address space enforced by the encapsulation mechanisms. No wonder: we basically inherited this type of OOP from operating systems programming people. Think of the Virtual Filesystem Switch in the BSD kernel, for instance. Any programming technique can be defended if it's successfully used in an operating systems kernel. This is a mode of argument known as ``proof by great hacker''.

You can view objects as isolates with their own thread of execution, like OS processes, but they aren't. There are systems that work that way -- ServiceOrientedArchitecture, and so, but they don't generally correspond to a language paradigm, because they are generally written in multiple languages. If you did have a language that actually worked that way, not just as a metaphor, you might have something sufficiently based on MessageOrientedProgramming to please AlanKay.
For consideration: is a language truly message-orientated, unless messages are first-class objects?

Circle and Ellipse

The dilemma of whether the relationship between circles and ellipses should be expressed via inheritance evaporates under the AlternateObjectOrientedProgrammingView. It's purely a symptom of a static type system, and to a lesser extent encapsulation. There is no reason why the ellipse type cannot be a supertype of the circle type. Clearly, every circle is an ellipse, allbeit an interesting special case of an ellipse.

The key to the tension in the circle-ellipse ``dilemma'' is that an ellipse object can be mutated so that it becomes a circle, yet its type remains ellipse because the static type system cannot express the idea that an object's type can change over its lifetime while its identity remains constant.

There are two solutions. One is to make the objects immutable, so that it's not possible to change an ellipse into a circle. The other is to allow mutation, but detect the case when an ellipse's properties indicate that it's really a circle and dynamically change its type to circle, taking care to add, delete and recompute any necessary properties to handle the representation change properly.

The second of these two solutions is difficult or impossible in a statically typed language. An object's type can't be changed; but perhaps some clumsy workaround can be invented to simulate the effect of a type change. For example, an object can be represented by two objects. One of them has a constant type and identity, and serves as a proxy to the other one, which is dynamically replaced as necessary. Encapsulation also gets in the way; obviously if we are going to handle the dynamic conversion of an ellipse to a circle, we have to know how to convert the properties of one to the other, and this means that the method for doing this will be quite "chummy" with both types. The logic for converting one class to another does not "belong" in either class.

One programming language that has good support for both of these solutions is CommonLisp. An in fact, its standard type system contains an isomorphism to the circle-ellipse relation. In its numeric tower, CommonLisp has the type rational which is subdivided into integer and ratio. Every rational number is either an integer or ratio; one cannot have an instance of the rational type which is not one of these two. An integer is clearly a special case of a ratio in which the denominator is one. But its representation is different, it's just one number. The relationship between rational, integer and ratio is exactly like ellipse, circle and non-circular-ellipse. Numbers are immutable in CommonLisp (like in most languages), so the language designers essentially opted for the first solution. It's not possible to mutate a ratio so that it becomes an integer. But of course, some computation over ratios can produce a result (in other words a new object) whose denominator is one; in that case, an integer object will be produced. For example (+ 1/2 1/2) yields the integer 1, but (+ 1/4 1/4) yields the ratio 1/2. A Lisp programmer can specialize a method parameter to rational, so that it captures both integers and ratios, or to the ratio type so that it only catches ratios, or to integer type so that it only captures integers. The inheritance relationship works well; there is no problem at all.


See also OoIsPragmatic


CategoryPolymorphism


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