Going Deeper with Objects
Going Deeper
There are other semantic features of OOP that we might want to have:
- inheritance, the ability to create sub-classes that inherit behavior and state from an existing class
- dynamic polymorphism, in which two objects that are instances of the same different classes can be used by the same client code because they implement a common interface
Let's consider polymorphism first, as it requires less new machinery.
Dynamic Polymorphism
What, if anything, do we have to do to implement dynamic
polymorphism using our closure-based objects? Racket is
dynamically typed, so we already have the ability to treat any
object like some other. Any selector procedure that accepts
withdraw and deposit messages can act
as a bank account. Client code wouldn't know the difference.
This means that dynamic polymorphism is free in our
implementation of OOP.
If we create a class-based implementation of OOP that checks types, we will need to implement a mechanism to allow polymorphic objects.
Classes and Inheritance
Inheritance is a challenge for this style of implementing OOP, because it does not explicitly represent the idea of a class.
If your only exposure to objects is through Python or Java, you may be surprised to learn that not all object-oriented languages have classes! Consider Self, a language designed at Sun Microsystems (now owned by Oracle) beginning in the mid-1980s.
In Self, there are no classes, only objects. You create a new object by "cloning" another object, called a prototype. You create a new kind of object by adding new state and behavior to an existing object. Self is quite cool and has influenced many languages since. You can do some things much more simply and elegantly in Self than in a class-based OO language.
The most popular and influential prototype-based language these days is Javascript. It is a language with broad application and remains a hotbed of development, thanks to the role it plays on the web.
In a dynamically-typed language such as Racket, classes might play a less important role than in a language such as Java. But how might we implement them?
If we think about classes in a different way, we can implement something simple that captures the idea. What if we think of a class as an object that can create instances for us? In that sense, we already know how to implement a class: use the same sort of closure that we use to implement objects!
If you'd like to see this idea in action, take a look at
this Racket file,
which defines make-account as a function that
responds to the message new. In response, it
returns a constructor for a bank account. It even has class
variables (often called 'static variables' in Java and C++)
that we can interact with through the class. Here is a sample
interaction:
> (define eugene ((make-account 'new) 100)) 100 > ((eugene 'balance)) 100 > ((eugene 'deposit) 100) 200 > ((eugene 'balance)) 200 > ((bank-account 'count)) 1 > (define mary ((make-account 'new) 100)) > ((bank-account 'count)) 2
In this approach, a class is a function that returns a new object to us. The new object is... a function.
Of course, this new style of bank account is a closure, too, so
we don't have to use the parenthesized syntax. Our original
version of send works with them:
> (define eugene (send make-account 'new 100)) > (send eugene 'balance) 100 > (send eugene 'deposit 100) 200 > (send eugene 'withdraw 50) 150 > (define mary (send make-account 'new 1000)) > (define alice (send make-account 'new 10000)) > (define bob (send make-account 'new 100000)) > (send make-account 'count) 4 > (send alice 'balance) 100000
We now have a way to implement multiple constructors, as we
see them in Java and C++. We can add another case to the
selector procedure returned by bank-account!
In Java, classes aren't objects to which we send messages;
class is a special construct. But in a language
such as Smalltalk, everything is an object. A Smalltalk class
is an object to which you send a message in order to create an
instance of the class. Today, languages like Ruby provide the
same feature. With one more level of closure wrapping our
object closure, we are able to implement a class with nothing
new. As David Wheeler, a programmer in the early 1950s (!)
once said,
Any problem in computer science can be solved with another layer of indirection.
Programmers of that time were solving many of the problems that cause us to bang our heads against the wall, and they did so at a time when the tools available were a lot less powerful. Often, constraints make us more creative.