Objects - the Self way

The previous section on objects treated objects by building up an object system by representing an object using a closure that dispatches on its first argument. While this is instructive, object systems are never done this way in practice for various reasons.

In particular, languages such as Self, Smalltalk, Javascript take on a philosophy of “everything is an object” and “anything that happens, does so by means of passing a message to an object”. In this section, we’ll see what it takes to have an object system like Javascript and Self – called a “prototype based object system”.

Prototypes versus classes

The notion of “class” is common in many “object oriented” programming languages such as C++, Java and Smalltalk. [1] In these languages, “classes” play the role of “object factories” - they make families of objects with similar kinds of properties and behaviour. In these langauges, a “class” can therefore be thought of as a “type” of an object.

One issue with such “classes and inheritance” based OOP languages is that an over-reliance on inheritance leads to an inflexible tangle of code as a system evolves into a large program. Though some design methods exist to help deal with this, the problem is particularly acute when we do not know the domain we’re modeling in adequate detail when we start out and we need to figure things out along the way. For this reason, the language Self (itself based on Smalltalk’s ideas) eschewed classes in favour of more flexible “prototypes”. An object in Self can “borrow” properties from a designated prototype and can delegate some message handling to its prototype. This is the model adopted (more or less) in Javascript. In such “prototype based” object systems, [2] an object behaves the “same” as its prototype in some regards and “overrides” some properties nad behaviours depending on its unique characteristics. Furthermore, one object can serve as a prototype for many other objects. Since “objects” are runtime entities, [3] this relationship between an object and its prototype is a dynamic relationship.

This is what we’ll model using plain #lang racket in this chapter.

What are objects in Javascript?

Javascript provides the following functionalities for its objects, which we’ll try to replicate in Racket.

Getting a property of an object

object.property_name

Given a property identified by a name, you can get the property of an object using the above “dot syntax” in Javascript. If an object itself does not have such a property, the runtime will search up the “prototype chain” of the object until it gets a value or else it produces an undefined value.

Modifying a property of an object

object.property_name = new_value;

In the above syntax, a new value is assigned as the property of the object, against the specified property name. It is important to note that post such an assignment, if object itself gains such a property if it didn’t have it earlier, even if its prototype did have a value for this property.

Invoke a method

result = object.method_name(arg1, arg2, ...)

In Javascript, as with most languages with object systems (except Erlang), the idea of “message passing” is treated as “method lookup and invocation”, which means synchronous function call. In the general case though, “message passing” could be asynchronous, which means a reply may or may not be delivered, instantaneously or later on.

The object.method_name part in the above Javascript syntax denotes retrieving an method function method from the prototype chain. This function is then called as though it were method(object, arg1, arg2, ...). [4]

Constraint

Once we have our object system’s core, we’ll place the constraint that anything we do must happen via a message send to an object.

The basic structure of objects

Taking the cue from the previous section, we conceive an object as having a set of properties and a reference to a “prototype”. We’ll model the “set of properties” as a simple list of name-value associations. An important thing to note is that objects are all about state management and so we’ll need all of these to be mutable.

#lang typed/racket

(struct Prop (name value attributes) #:mutable)
(struct Obj (properties prototype) #:mutable)

(define (lookup name props)
    (if (empty? props)
        #f
        (if (equal? (Prop-name (first props)) name)
            (first props)
            (lookup name (rest props)))))

In the above scheme, Prop is a triple consisting of a name symbol, a value associated with that symbol and a list of attributes which are themselves Prop structures.

Also Obj is a tuple of properties which is a list of Prop and prototype which is itself an Obj.

Now, we need to model the three basic operations on objects provided by Javascript. Let’s do them one by one. First up is get, which is expected to retrieve the value associated with a given property name, and if the object itself does not have such a property, look it up in the prototype chain.

(define (get obj propname)
    (let ([p (lookup name (Obj-properties obj))])
        (if p
            (Prop-value p)
            (get (Obj-prototype obj) propname))))

Next up is set which should associate the given property name with the given value for the given object. Note that it can’t use get above for the lookup since it might then end up modifying a prototype’s property, which we won’t want to.

Think now

So what if the prototype’s property gets modified?

(define (set obj propname val)
    (let ([p (lookup name (Obj-properties obj))])
        (if p
            (set-Prop-value! p val)
            (set-Obj-properties! obj
                                 (cons (Prop propname val empty)
                                       (Obj-properties obj))))
        obj))

The last major piece is the “message passing”. We’ll call this “send” and denote it using the ! symbol for brevity … and also to suggest that all message sends could potentially change state.

(define (! obj selector . args)
    (let ([methodfn (get obj selector)])
        (apply methodfn (cons obj args))))

It is kind of amazing that we’ve covered nearly all of the object mechanism in Javascript at the foundation level now! What remains (and that’s not trivial still) is to build the object system around these primitives. We’ll need to establish many conventions along the way to make that happen and also “bootstrap” the object hierarchy.

(define (make proto)
    (Obj empty proto))

(define (new maker . args)
    (let ([obj (make maker)])
        (apply ! (cons obj (cons 'init args)))))

The above new procedure mimics what happens in the Javascript “new” operator which is used like new Something(arg1, arg2, ...) to manufacture new objects from the given “constructor function” named Something. In essence, what it does is two steps -

  1. Allocates space for the object, initializing its prototype chain.

  2. Calls its init method with the given args.

Our implementation of new models both these steps explicitly – with the allocation step handled by make and the initialization step by the 'init message send.

Creating the object system

With the foundations in place, we now need actual objects to use as prototypes when making new objects. We also need to live by our maxim that everything we do must be done by message passing after we’ve created the basic object system.

To start with, we need to answer the question of what exists at the end of all prototype chains. Given an object, when you pick out its prototype and then its prototype and so on, where does that process end? In Javascript, that ends with the Object object.

Note

Yeah. Terminology in object oriented systems gets rather confusing to keep in head correctly. Objects may have classes and these classes may themselves be objects, and so on. We’ll spare ourselves that confusion for the moment as we work through this.

(define Object
    (let ([O (make #f)])
        (set-Obj-prototype! O O)
        (set O 'proto (λ (self)
                          (Obj-prototype self)))
        (set O 'init (λ (self . args)
                        self))
        (set O 'display (λ (self)
                            (display "Object")))
        O))

What we’ve done here is a “bootstrapping” step. We’ve made an object bound to Object whose prototype is itself. We can now make all objects using Object as their prototypes. Note that we’ve endowed all objects with the ability to retrieve their “prototype” objects for reference anywhere we need it, and we can now do that within our object system using a message send.

Note

While we’ve so far stuck to the Javascript object model of a prototype based object system, we deviate from that in the sections below in the interest of keeping our system minimal, and to also illustrate approaches used in other object systems such as Smalltalk which take the constraints of “everything is an object” and “everything happens via message passing” far more seriously than Javascript does. Javascript, in this sense, is closer to Scheme than it is to something like Self.

Let’s start with a simple one for the primitive entities we’ll need in our object system - numbers, strings, booleans and code blocks.

(define Num
    (let ([N (new Object)])
        (set N 'init (λ (self val)
                        (set self '_value val)
                        self))
        (set N 'display (λ (self)
                            (display (get self '_value))
                            self))
        (set N '+ (λ (self n)
                    (new Num (+ (get self '_value)
                                (get n '_value)))))
        (set N '* (λ (self n)
                    (new Num (* (get self '_value)
                                (get n '_value)))))
        (set N '- (λ (self n)
                    (new Num (- (get self '_value)
                                (get n '_value)))))
        (set N '/ (λ (self n)
                    (new Num (/ (get self '_value)
                                (get n '_value)))))
        (set N 'sqrt (λ (self)
                        (new Num (sqrt (get self '_value)))))
        (set N 'square (λ (self)
                            (let ([x (get self '_value)])
                                (new Num (* x x)))))
        N))

In the above definition of Num, which uses the Object as its prototype, we use a convention that the _value property stores a native Racket numeric value rather than an object – using the _ prefix to remind us of that. In general though, we want to stay within the object system, but we can do that only once we have the common primitive types we need for ordinary programming.

Next up is booleans. It might look like we need to make an object named Bool or something which has some properties. However, that isn’t of much use since we need to consider what we need to be able to do with booleans – i.e. implement conditionals. So similar to how we modeled booleans in lambda calculus as selector functions, we can define two new prototypes for True and False that will serve as the boolean values in our system.

(define True
    (let ([T (new Object)])
        (set T 'and (λ (self b) b))
        (set T 'or (λ (self b) True))
        (set T 'not (λ (self) False))
        (set T 'display (λ (self) (display "True") self))
        T))

(define False
    (let ([F (new Object)])
        (set F 'and (λ (self b) False))
        (set F 'or (λ (self b) b))
        (set F 'not (λ (self) True))
        (set F 'display (λ (self) (display "False") self))
        F))

We also need a way to encapsulate blocks of code as objects. We have a choice at hand – we can either express such a block of code as a data structure that is wrapped as an object, or we can fall back on an ordinary Racket function wrapped as an object. Since you already know how to design such a “code as data structure” and implement such an interpreter for it, we’ll take the latter simpler route. So what can we do with a block? We can “execute” it. By that, we mean we’ll take the Racket function stored as the “block“‘s value and call it with some arguments. Under normal circumstances, an ordinary Racket function will do, but in this case, we’ll need the ability to do premature returns from our code blocks, because we may have blocks within blocks and the return path could be non-linear. To meet that, we need to pass an explicit return argument to the function when invoking it so that such premature returns can be done. We know how to construct such a return argument – we use call/cc.

(define Block
    (let ([B (new Object)])
        (set B 'init (λ (self fn)
                        (set self '_value fn)
                        self))
        (set B 'display (λ (self) (display "Block") self))
        (set B 'exec (λ (self . args)
                        (call/cc (λ (return)
                                    (apply (get self '_value)
                                           (cons return args))))))
        B))

Conditional execution

All that is fine, but how do we do things like if or equivalently cond within our object system without again resorting to Racket Blocks give us immense flexibilty there and we can maybe think of putting if expressions within these blocks, but that would be cheating. We’d be relying too much on Racket’s facilities and not really making our point that we can do everything we need to within our object system.

Indeed, this is possible with the way we’ve define Block and our booleans. We need to add new methods to them.

To execute a block conditional on a boolean value, we’ll send an if-true message to the boolean object passing a block as argument. If the message is sent to True, the block should be evaluated, but if it is sent to False, ir shouldn’t be.

(set True 'if-true
    (λ (self block)
        (! block 'exec)))

(set True 'if-false
    (λ (self block) self))

(set True 'ifelse
    (λ (self trueblock falseblock)
        (! trueblock 'exec)))

(set False 'if-true
    (λ (self block) self))

(set False 'if-false
    (λ (self block)
        (! block 'exec)))

(set False 'ifelse
    (λ (self trueblock falseblock)
        (! falseblock 'exec)))

So now, if we have a boolean value (i.e. True or False) bound to an identifier c and we wish to execute a block blk depending on whether the boolean is true, all we need to do is (! b 'if-true blk).

Loops

Now that we have blocks that know how to execute themselves and produce object results, we can use this framework to implement a while loop like this –

(! condblock 'while-true bodyblock)

… in which we expect the condblock to be evaluated repeatedly and as long as it is true, we’ll continue to execute bodyblock. This is simple to implement as a feature of the Block object.

(set Block 'while-true
    (λ (self body)
        (let ([c (! self 'exec)])
            (! c 'if-true (new Block
                            (λ (return)
                                (! body 'exec)
                                (! self 'while-true body)))))))

Granted, this is pretty inefficient, creating a new Block object for every loop iteration, but we put up with that to stay true to our commitment of doing everything with objects and message passing.

(set Num '< (λ (self n)
                (if (< (get self '_value) (get n '_value))
                    True
                    False)))

(set Num '= (λ (self n)
                (if (equal? (get self '_value) (get n '_value))
                    True
                    False)))

(set Num '<= (λ (self n) (! (! self '< n) 'or (! self '= n))))
(set Num '>= (λ (self n) (! (! self '< n) 'not)))
(set Num 'succ (λ (self)
                    (new Num (+ (get self '_value) 1))))
(set Num 'pred (λ (self)
                    (new Num (- (get self '_value) 1))))

(set Num 'times-do
    (λ (self block)
        (! (! self '> (new Num 0))
           'if-true
           (new Block (λ (return)
                        (! block 'exec)
                        (! (! self 'pred) 'times-do block))))))

You can now see how the whole system can start working together and how to create the other “primitive” object types we’ll need for regular programming such as strings.

(define String
    (let ([S (new Object)])
        (set S 'init (λ (self str)
                        (set self '_value str)
                        self))
        (set S 'display (λ (self)
                            (display (get self '_value))))
        (set S 'concat (λ (self str)
                            (new String (string-append (get self '_value)
                                                       (get str '_value)))))

        ; Add your own methods too.
        S))

Let’s now define an aggregate object called Point that holds two numbers – the x/y coordinates of the point – as an illustration./

(define Point
    (let ([P (new Object)])
        (set P 'init (λ (self x y)
                        (set self 'x x)
                        (set self 'y y)
                        self))
        (set P 'dist (λ (self p)
                        (! (! (! (! (get self 'x) '- (get p 'x)) 'square)
                              '+
                              (! (! (get self 'y) '- (get p 'y)) 'square))
                            'sqrt)))
        P))

Note that the x and y properties are expected to hold objects and not Racket primitive numbers. We can finally live within our object system! We’ll now extend the functionality provided by Point to make a mathematical “vector” that knows how to calculate its own length.

(define Vec
    (let ([V (make Point)])
        (set V 'length
            (λ (self)
                (! (! (! (get self 'x) 'square)
                      '+
                      (! (get self 'y) 'square))
                   'sqrt)))
        V))

Now we can do (! (! (new Vec (new Num 3.0) (new Num 4.0)) 'length) 'display) to get 5.0 printed out. We could’ve also defined the Vec like this –

(define Vec
    (let ([V (make Point)])
        (set V 'length
            (λ (self)
                (! self 'dist (new Vec (new Num 0.0) (new Num 0.0)))))
        V))

Observations

  1. Note that we’ve augmented the various core classes with new functionality after we created them. This means any object created that uses one of these objects as its prototype will automatically “inherit” the newly added functionality. Not all object systems permit this kind of extension, but all prototype-based object systems do.

  2. Taking the example of the 'times-do method, we see that we could’ve also defined that as a method on a Block object like shown below –

    (set Block 'times-do
        (λ (self num)
            (! (! num '> (new Num 0))
               'if-true
               (new Block (λ (return)
                             (! self 'exec)
                             (! self 'times-do (! num 'pred)))))))
    

    How do we decide which of the two approaches to take, from a designer’s perspective?

    This is one of the issues that plagues all “object oriented” systems. Since we can only choose a method based on one selector, we’re forced to make a choice about which object to place the method implementation for that selector in, even if there is no such clear choice suggested by the domain. In this sense, object systems create some artificial asymmetries in theory. However, in practice, it turns out you can do a lot even given this ambiguity and as long as the methods are documented well, programmers don’t have much trouble using the object system.

    In the case of times-do however, the language suggests a message structure like (! (new Num 3) 'times-do ...block...).

  3. Since we now have the ability to make new objects using an existing object as its “recipe” (as embodied in the implementation of the 'init method), these base objects such as Num and True could be called “classes”.

  4. It is a useful exercise to try and redo the above code in #lang typed/racket. That way, you can use the Racket type system to enforce our constraints of “everything is an object” and “everything happens through message passing” that we imposed on ourselves. Well, it is hard to do the latter thoroughly, but it is still worth the effort to clarify the object system built above. You’ll have to carefully maintain a separation between the object system and Racket-primitive values, unlike what we’ve assumed we can do here.

    Exercise

    Reimplement this object system using #lang typed/racket.

  5. If you looked carefully at our Vec, you’ll notice that the second implementation of length method makes use of the fact that both Vec and Point have the coordinates available in the x and y fields to borrow the dist implementation to calculate length. i.e. a Vec is usable as a Point because both have similar properties. This is referred to in some languages (such as Ruby) as “duck typing” – taken from “if it looks like a duck and quacks like a duck, it is a duck”.