Everyday objects: Things we encounter everyday
Properties:
Conclusion: It is thus good to have programs create entities like these objects we are familiar with.
this in Java/C++ and self in Smalltalk).
Consider a point, with x, y fields and methods
magnitude and iszero with obvious functionality.
point = { x = 4; y = 7; magnitude = Function _ -> ???; iszero = Function _ ->???}
magnitude needs access to x and
y, but it currently has no name for them.
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.
point itself is passed as an argument to the
message send.
zero is a method which calls magnitude --
we need to pass along this, which was originally passed
to us, on the magnitude so it can know itself as well.
obj <- methodwhich stands for
(obj.method) obj
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.
Example:
Person with subclasses
Mother, Father, Child.
tallerThan(aPerson) =
this <- height >= aPerson <- height
could be a method of a
Person and is passed another Person as
argument.
Child,
Mother, or Father since they all have a
height.
Let grabsnork = Function r -> r.snorkcan take any record with a
snork field:
.. In grabsnork ({snork = 4, moo = 4, wapp = 44})
Let eqpoint =
{ ... // all the code from point above: x,y,magnitude,...
equal = Function this -> Function apoint -> !this.x=!apoint.x
And !this.y =!apoint.y}
In eqpoint <- equal({x = Ref 3; y = Ref 7; /* anything else or nothing */})
--the "point" passed to equal need only have x and
y.
Dinosaur objects also had a
height method; a aPerson
<-tallerThan(aDinosaur) would then execute without error.
tallerThan could even get
aDinosaur as argument!), we don't know
precisely where the methods will be laid out in memory.
We want to protect some methods/instances from direct outside use.
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 5This solution is somewhat flawed.
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.
Simple class encoding, ignoring hiding.
Its trivial: freeze the
object-building code so it can be repeatedly executed.
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.
point1 and point2 have their own
x and y values.
Ref gives you a fresh memory
reference every time you use it.
Point pasted into ColorPoint
Point, that change
automatically gets reflected in the
Point code pasted into ColorPoint
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 ...
colorPointClass () will make an object with the appropriate
methods.
super.magnitude this ()Note how in the above interpretation all of the inherited methods are explicitly listed by name. This is not the best form of encoding, but is as good as we can do in the current language: there is no operation to "tack on" fields to an arbitrary record, or to append two records.
Let cp = colorPointClass () In cp.setx cp 5; cp.sety cp 5; cp.magnitude cp ()
o.m in the code, precisely what
method m will be run at run-time.
v could contain many different kinds of objects, so
v.m could be sending m to one of those
different kinds of objects and thus unless you know the object,
you don't know which method is being run.
isNull in pointClass and
inherited by ColorPointClass, with code
Function _ -> (this.magnitude this) = 0the
magnitude method
performed here is not
fixed at compile time: if this
is a Point, the brightness is not taken into account, but
it is if this is a ColorPoint. This is
known as dynamic dispatch.
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
The alternative of not adding any new syntax and directly programming, is
simply too confusing for the programmer (esp. after types are added!).
DOB has the following features
Here is the point/colorpoint example in DOB's concrete syntax.
Points to be made about the DOB language syntax details:
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.
--For example, the encoded colorPointClass above is just too
hard to read.
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.
New aClass" is an
Object.
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.
Here is the datatype for DOB.
This and Super are now "reserved
variables": in the encoding "Function this -> ..." had
to be explicitly written; that is implicit by having This a
special variable.
EmptyClass, an empty class.
Object in the example above).
initialize
method. Instance variables have to be given some initial value. A
"better" implementation of the above classes would be as
Function x -> Function y -> Class ... Inst x = x, y = y, ....
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 * termThe 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. newx above, are just
Var (Ide "newx").
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.
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.