Objects, Classes, and Inheritance

Simple Objects

Why Objects Anyway? Thesis: Humans as part of their basic function are highly adept at recognizing and interacting with everyday objects.

Everyday objects: Things we encounter everyday

Properties:

Objects in OOP (Object-Oriented Programming) also have all of these features.

Conclusion: It is thus good to have programs create entities like these objects we are familiar with.

The structure of an object

An object is Other aspects for objects We will come back to some of these later: We will uncover these issues by encoding objects in the DSR language.

Encoding simple objects in DSR

Slogan: Objects are records of functions and refs First pass at objects in DSR

Consider a point, with x, y fields and methods magnitude and iszero with obvious functionality.

point = { x = 4; y = 7; magnitude = Function _ -> ???; iszero = Function _ ->???}
Object then is
point = { x = 3; y = 4;
          magnitude = Function this -> Function _ -> sqrt(this.x + this.y);
          iszero = Function this -> Function _ -> this.magnitude (this) = 0   
}
Message send then is
point.magnitude(point){}
Observations on the above. We can write a message send abbreviation
obj <- method
which stands for
(obj.method) obj

Mutable Instance Variables

Problem: These objects are immutable! Solution.
point = { x = Ref 3; y = Ref 4;
             magnitude = Function this -> Function _ -> sqrt(!(this.x) + !(this.y)),
             setx = Function this -> Function newx -> (this.x) := newx,
             sety = ... }
We can now say
point.setx (point)(0);
point.magnitude (point){}; (* returns 4 now, objects state changed *)
point.x := 6;              (* directly change fields *)
!(point.y)                 (* directly get field values *)
In general, this strategy gives a faithful encoding of simple objects.

Let us pause a bit and map some classic notions of object on to this paradigm. Consider encoding sets, stacks, students (as records in a database), etc.

Object Polymorphism

Object Polymorphism means an object passed to a method as an argument can be of a form different than the form declared (or in an untyped language, originally intended) in the method.

Example:

Observations Dispatch issues that arise with object polymorphism:

Private and protected Methods/Instances

(we skipped this topic in lecture)

We want to protect some methods/instances from direct outside use.

A partial encoding of hiding in our DSR objects
Let point = ... In
Let hiddenPoint = { magnitude = point.magnitude point;
                        setx = point.setx point;
                        sety = point.sety point }
--each method is "pre-applied" to the full point, and the hiddenPoint only contains the public methods/instances. Methods are now invoked simply as
hiddenPoint.setx 5
This solution is somewhat flawed.
Methods that return this re-expose the hidden parts.
point = { x = Ref 3; y = Ref 4;
             magnitude = Function this -> Function _ -> sqrt(!(this.x) + !(this.y));
             setx = Function this -> Function newx -> (this.x) := newx;
             sety = ...;
             sneaky = Function this -> Function _ -> this }
Let hiddenPoint = {magnitude = point.magnitude point;
                       setx = point.setx point;
                       sety = point.sety point;
                       sneaky = point.sneaky point }
hiddenPoint.sneaky {}
now returns the full point object, oops!

A better solution In order to solve this problem we must use a different style of encoding, where we don't pass this evey time a method is invoked. Instead, the object gets a pointer to itself at the start.

Let prePoint =
  Function this -> Let privateThis = { x = Ref 3; y = Ref 4 } In
   { magnitude = Function _ -> sqrt(!(privateThis.x) + !(privateThis.y));
     setx = Function newx -> (privateThis.x) := newx;
     sety = Function newy -> (privateThis.y) := newy }
     notSneaky = Function _ -> this }
In Let point = prePoint prePoint // make the object self-aware forevermore.
In ...
and message send simply as
point.magnitude ()
Method notSneaky returns this, but it is the public parts of this only. Note, for this encoding to hide properly, you can only send messages to privateThis, not return it.

These encodings are relatively easy; encoding classes and typed objects as well is much more difficult.

Classes

Let pointClass = Function _ ->
             { x = Ref 3; y = Ref 4;
               magnitude = Function this -> Function _ -> sqrt(!(this.x) + !(this.y));
               setx = Function this -> Function newx -> (this.x) := newx;
               sety = ... }
In ...
To create a new object of aClass, just thaw the above:
new aClass is defined as aClass ()
Then, some code like
Let point1 = pointClass () In point2 = pointClass () In point1.setx 5 ...
will create and use instances of pointClass. Hiding in classes: apply above freeze trick to object encodings above -- also easy.

Inheritance

80% of the usefulness of object-oriented programming comes from the above concepts: 20% is the idea of As you well know, We can encode inheritance in DSR Example: ColorPoint class.
Let pointClass = ... as above ... In
Let colorPointClass = Function _ ->
          Let super = pointClass () (* create slave point "super" for our use *) In
             { x = super.x; y = super.y; (* grab these fields from slave *)
             color = Ref  { red = 45; green = 20; blue = 20 };
             magnitude =
                Function this -> Function _ -> super.magnitude this () *
                                   this.brightness this;
             brightness = Function this -> Function _ -> ... compute brightness  ...;
             setx = super.setx (* grab method from superclass slave *)
             sety = super.sety; setcolor = ... }
In ...
Test code to think about to see if we believe the encoding above:
Let cp = colorPointClass () In
  cp.setx cp 5;
  cp.sety cp 5;
  cp.magnitude cp ()

Dynamic Dispatch

Methods are dynamically dispatched when its not clear from looking at a message send o.m in the code, precisely what method m will be run at run-time. In Java (and every other object-oriented language), you always get appropriate dynamic dispatch. In C++, if you don't declare a function virtual, you get no dynamic dispatch.

Inheritance should model the cut-and-paste view of code, and if the method were pasted, the dispatch would change. This view is sound only if inherited methods get a revised notion of this upon inheritance.
Does our encoding promote dynamic dispatch?

Adding Object-Orientated Features to D: DOB

Now that we have given encodings for objects in terms of known syntax, we can study how these features may be directly added to the D language to give DOB, object-oriented D.

The alternative of not adding any new syntax and directly programming, is simply too confusing for the programmer (esp. after types are added!).
--For example, the encoded colorPointClass above is just too hard to read.

DOB has the following features

Primitive objects aren't very common in practice; here is why we include them. Assertion: Object-oriented languages of the future should include syntax for primitive objects. Also gives much of advantage of higher-order functions to object-oriented language. Java's inner classes are about 95% there.

Here is the point/colorpoint example in DOB's concrete syntax.

Let pointClass =
      Class Extends EmptyClass
            Inst
              x = 3;
              y = 4
            Meth
             magnitude = Function _ -> sqrt(x + y);
             setx = Function newx -> x := newx;
             sety = ...
     
In Let colorPointClass =
      Class Extends pointClass 
            Inst
              x =  3;
              y =  4;
              color = Object Inst Meth red = 45; green = 20; blue = 20
            Meth
              magnitude =
                Function _ -> Super <- magnitude ()) *
                        This <- brightness;
              brightness = Function _ -> color <- red + color <- green + color <- blue;
              setx = Super <- setx (* explicitly inherit *)
              sety = ...; setcolor = ... 
     
In Let point = New pointClass 
In Let colorPoint = New colorPointClass In
  point <- magnitude(); point <- setx 4; colorpoint <- magnitude () 
End
This syntax is derived from the encoding of objects and classes above.

Points to be made about the DOB language syntax details:

Here is the datatype for DOB.
datatype ide = Ide of string | This | Super (* this & super are special id's *)

datatype label = Lab of string

datatype term = (* ... insert D syntax elements here ... *)
| Object of ((label * term) list) * ((label * term) list) (* instances list and methods list *)
| Class of term * ((label * term) list) * ((label * term) list)
| EmptyClass | New of term
| Send of  term * label
| InstVar of label  (* parser has to decide if a var is InstVar or just Var *)
| InstSet of  label * term
The relation is mostly clear; the one perhaps tricky bit is InstSet(Lab"x",exp) is the abstract syntax for concrete syntax x := exp, and InstVar(Lab "x") corresponds to x for x an instance variable access.
Note that regular variables that are parameters to functions, such as newx above, are just Var (Ide "newx").

Translating DOB into DSR

The translation of DOB into DSR follows the ideas of the case-by-case informal encodings given above. Actual object-oriented compilers do not use this methodology now, but it is a valid one and is much more "methodological" and leads to a more understandable view of object-oriented language compilers.

This section: translate DOB into DSR via a function toDSR. By implementing this translation, writing a parser for the DOB syntax, and using your DSR compilers, a DOB compiler is obtained:

DOBCompile = toC o hoist o Atrans o cc o toDSR ;
where toC etc are the components of the DSR compiler.

The Official Translation

The DOB-to-DSR translation function is inductively defined as follows.
toDSR Object Inst x1 = e1, ..., xn = en Meth m1 = e1', ... mk = ek' =
   { inst = { x1 = Ref (toDSR e1); ....; xn = Ref (toDSR en) };
      meth =  { m1 = Function this -> toDSR e1'; ....; mk = Function this -> toDSR ek' } }
toDSR Class Extends e Inst x1 = e1, ..., xn = en Meth m1 = e1', ... mk = ek' =
   Function _ -> Let super = (toDSR e)() In 
    { inst = { x1 = Ref (toDSR e1); ....; xn = Ref (toDSR en) };
      meth =  { m1 = Function this -> toDSR e1'; ....; mk = Function this ->  toDSR ek' } }
    
toDSR New e      = (toDSR e) () 
toDSR EmptyClass = Function _ -> {} 
toDSR Super <- m =  super.meth.m this
toDSR e <- m  =  Let ob = (toDSR e) In ob.meth.m ob, for e not super
toDSR x := e     = this.inst.x := (toDSR e)  (* notice how the LHS is an instance x, not an arb. expression *)
toDSR x          = !(this.inst.x), for x an instance variable
toDSR y          = y, for y a function variable
toDSR <others>   = homomorphic
The translation is fairly clean except message sends to Super have to be handled slightly differently to give dynamic dispatch.

Instance variables x and method arguments y are different sorts in DOB: their translation is different.

As an exercise, feed the above point/colorpoint example through this translation. The code produced will be very similar to the initial informal translation given.
Note, officially f (), empty-argument function application, may be written f {} (empty record application), and _ in Function _ -> e is any variable not occurring in e.

A Direct Interpreter for DOB

(this topic will be skipped in lecture)

The idea of writing the interpreter is to use the encoding as a guide. We will blissfully ignore dealing with stuck states properly, by using incomplete pattern matches.

First, objects.

...
| eval(Object(meths,insts)) = Object(meths,evalInsts(insts))
...

and evalInsts l = (* evaluate the list of instances, return evaluated list *)
Message send
| eval(Send(term1,label)) =
  let val1 = eval(term1) in
  case val1 of Object(insts,meths) ->
     subst(selectMeth(meths,label),val1,This)
Classes (only considering the case now of an empty superclass)
fun e(Class(super,insts,meths)) = Class(super,insts,meths)
 |  e(New(Class(MTClass,insts,meths))) = eval(Object(insts,meths))
...

Important features we skipped

Typing

It is relatively easy to understand objects and classes in terms of records of Function's and Ref's. It is a lot harder to understand typed objects in terms of typed records of Function's and Ref's.

Typing objects will briefly be discussed later in the course.

Class Names

Classes in the above are not given unique names: the Class syntax has no field which is the class name. Pretty much every actual object-oriented language has a notion of class name which means something.

In Java, for instance, the class (and interface) names are important relative to casting: you can only cast an object to its super or sub-class.

Constructors and Destructors

In our language there is not so much need for constructors and destructors. Functions can return classes, and that method allows a form of constructor to be programmed:
Let Point = Function xinit -> Function yinit -> Class .... x = xinit, y = yinit, ...
Then, a new point (3,4) is created by New (Point 3 4).
This is another advantage of having higher-order functions around.

Objects in OCaml

We briefly cover the OCaml object system: see The Manual.

Key features: