Organizing Objects

Clone and Copy

The basic object creation protocol in Slate, as in other prototype-based languages such as Self, is to clone or copy an existing object to get a new one, and extend this new object until it has the required functionality.

Let us suppose we have the following object setup:

Figure 1-4. Sample object setup

For the curious, this setup is quite common and is obtained by issuing the following command

Slate 2> addPrototype: #x derivedFrom: {Cloneable}.
("x" traits: ("x" traits: ("Traits" ...). printName: 'x'. parent0: ("Cloneable" ...)))
Slate 3> 

Deriving objects will be described shortly. Suppose we now clone the object x with the command

Slate 3> addSlot: #y valued: x clone.
(y . prototypes . globals . Mixins .
        Types . x . VM . traits )
Slate 4> 

The object setup is now as follows:

Figure 1-5. Object setup after cloning

Cloning performs a bitwise copy of the old object to produce the new one. The new object then gets the same number of slots and roles, with exactly the same values for these slots and roles.

Each object in Slate has a delegation slot called traits. You can then think of objects as instance/traits pairs (but the relationship between an object and its traits is not that which exists between an instance and its class in other languages). The traits object is used to constitute families of objects. In any family of objects, the trait object is used to encode structure and behavior that is common to all objects in the family. This is parallel to the classical class mechanism, where all instances of a given class share the behavior defined by the class.

To specialize one object in the family, you can just add slots to it, or define new methods on it. The traits object for the family acts as a pool for common behavior, that each object in the family is free to use or not through delegation. In the preceding diagram, any slots or roles that are stored on x traits are shared between the objects x and y, while each of them still has its own sets of slots and roles available for specialization.

Note

While it is possible to globally restrict access to a slot by declaring it with the #addImmutableSlot:valued: method, there is no direct equivalent of this method to restrict access to roles. Controlling access to the roles (and thus restricting or overriding execution of methods) can be done through the use of subjects and layers. In fact, subject and layers offer more generality than the simpler #addImmutableSlot:valued: mechanism, and can be used to restrict both slots access and roles access to certain objects. This reminds the design by contract notion used in Eiffel.

To be cloneable, a Slate object has to have the prototype object Cloneable in its delegation graph.

Cloning is a bitwise object copy, maybe with some precondition checked to allow or disallow the copy to be performed. You may think of cloning as a controlled shallow copy. The role (1, #shallowCopy) is stored on the Root object, making it available for every object in the system, and is defined to select the clone method on that object. Copying performs a deeper copy: it will try to duplicate the values of specific slots (calling copy or clone on them) onto a new object. The creation of the new object relies internally on cloning, so the traits are shared between the object and its copy, but the roles specific to the object are copied over to the new object.

Oddballs

Although the vast majority of objects are cloneable, not all objects must be cloneable. For instance, the True and False objects, which stand for boolean truth and falsehood, must be unique for any logical predicate to work. Another example of an object that should not be cloneable is Nil, which models the absence of any value.

The objects that cannot be cloned are called oddballs, after the name of the prototype which implements the impossibility to clone. Each existing oddball in Slate has this prototype in its delegates' list. For example:

The object Nil has 1 slot called traits.

Slate 2> Nil slotNames.
{#traits}
Slate 3> 

Nil delegates to its only slot.

Slate 3> Nil delegateNames.
{#traits}
Slate 4> 

Nil's only delegate is Oddball traits.

Slate 4> Nil traits == Oddball traits.
True
Slate 5> 

Note

Currently, because of a known problem in Slate inheritance, the clone message is available for any Slate object, including Oddballs, and always produce a new, bitwise identical copy of its receiver. You should therefore be extra careful when the result of a clone is used inside a test, since all of the following expressions return False.

Nil clone == Nil
True clone == True
False clone == False

For reference, the Self object system has the same problem.

Deriving

When you design your graphs of objects, you will often need sub-families in other families of objects, in order to extend the objects in the sub-families. Although you can achieve this by copying and adding delegation slots, this behavior is encapsulated in one single operation, since it is very common. This operation is called derivation. In the following figure, y derives from x, and is produced by the following expression:

Slate 3> addSlot: #z valued: x derive.
(z . prototypes . globals . Mixins .
        Types . x . VM . traits )
Slate 4> 

Figure 1-6. Object derivation

You can thus create hierarchies of objects, sharing slots and behavior. These hierarchies are of a different nature from what you get with class hierarchies. Instead of walking up a class hierarchy by stating that each subclass "is a kind of" its superclass(es), you walk up hierarchies of objects by stating that each object "is akin to" other members of its family and "stems from" upper-level families. You get thus a kind of genealogy of objects.

This can be further refined, as it is possible for an object to have several delegates, or to be part of several families.

As an exercise in understanding the object relationships, let us explore the connection between Cloneable and Derivable. This can be done by using the object inspector.

Slate 2> load: 'src/lib/inspect.slate'.
Loading 'src/lib/inspect.slate'
You are in a twisty little maze of objects, all alike.Nil
Slate 3> 

This loads the inspector inside the image. Starting the inspection is done as follows:

Slate 3> inspect: Derivable.
("Inspector" inspector: ("Inspector" history: {"Stack" ...}. it: ("Derivable" ...). traits: ("Inspector" ...)). prototypes: (CompiledMethod .
                ByteArray . conditions . Traits . Root .
                mappings . Oddball . Boolean . Syntax .
                streams . Namespace . CharacterCollection . Derivable .
                Map . PrimitiveMethod . ExternalResource . ...).
        globals: (features . Image . Symbols . bootstrapInterpreter .
                True . traits . bootstrapSymbols . False .
                Console . lobby . bootstrapCharacters . Nil .
                specialOops . NoRole ). Mixins: (PrettyPrinterMixin . Mixin .
                CollectionTypeMixin . NumericMixin . traits ). Types: ("Inspector" Block: ("Block" ...).
                Intersection: ("Intersection" ...). Any: ("Any" ...). Union: ("Union" ...). Type: ("Type" ...).
                Array: ("Array" ...). Range: ("Range" ...). traits: (...). lobby: ("Inspector" ...).
                None: ("None" ...). Singleton: ("Singleton" ...). Not: ("Not" ...). Clone: ("Clone" ...).
                Member: ("Member" ...)). VM: (ByteCompiler . ByteCode . traits ).
        traits: (traits . printName . parent0 ))
Slate 4> 

Now we are inside the inspector. All commands we issue are first dispatched to it, until we finish the inspecting session with a close command. For example, let us get the slots and delegates of Derivable.

Slate 4> slots.
{#traits}
Slate 5> parents.
{#traits}
Slate 6> 

Let us go and inspect this #traits slot.

Slate 6> go: #traits.
("Derivable" parentNames: {#parent0. #parent1. #parent2. #parent3.
                #parent4. #parent5. #parent6. #parent7.
                #parent8. #parent9}. printName: 'Derivable'. parent0: ("Root" printName: 'Root'.
                traits: ("Traits" ...)). traits: ("Traits" traits: ("Traits" ...). printName: 'Traits'. parent0: ("Cloneable" ...)))
Slate 7> 

To go back to the previous object, the back command may be used. Other commands to rewind contexts are backToStart, which goes back to the object we started inspecting, and back:, which takes an integer as argument and tries to rewind this number of contexts. One last command is it, which will redisplay information about the object being currently inspected.

Inside the inspector, it is possible to run normal Slate code. This can be useful, for example, to make sure if we are inspecting some object or its traits, as they can be a little hard to distinguish at first.

Slate 7> it == Derivable.
False
Slate 8> it == Derivable traits.
True
Slate 9> 

Slots addition or removal or method manipulation can also be performed from inside the inspector. One just need to be careful that the execution context is the inspector itself, and not the lobby or some other namespace.

The relations between Derivable and Cloneable is then as follows.

Figure 1-7. Cloneable and Derivable

Prototypes and traits

Some objects in the system are abstract: their role in the system is to collect slots and behavior common to other objects. They capture how other objects resemble each other, they are the traits of the other objects.

Suppose you see an elephant for the first time. If you try to model it with an object, you would put all attributes and behavior attached to this new specimen on the object itself. But if you then see another elephant, instead of repeating the whole description of the elephant on this second specimen, you would notice the two are alike, and model this with another object.

That object would not stand for any real elephant, but would hold all that elephants have in common, the traits that make them be elephants. It would be the traits object for elephants, and the two real elephants you met would delegate to this traits object for slots and behavior common to all elephants, while having also slots and behavior to model their specificities, like their age or height.

Note

The ability to easily go from the concrete to the abstract when trying to model a problem is a property of prototype-based object systems. It gives you more freedom as you are not forced to guess abstractions (such as classes) first and then try to match the reality of the problem at hand. Instead, an exploratory style of development is encouraged, just as in the example with the elephants.

Although traits collect slots and behavior for other objects, they are not the same concept as classes in classical class hierarchies. An object is not an instance of its traits objects, nor does it rely solely on its traits objects to get its slots and behavior, as it is possible to define slots and behavior directly on the object itself without using its traits objects.

Creating a traits object is often transparent. When you copy or derive an object, the new object will have a traits slot referencing its traits object. The traits object will be the same as the traits of the initial object in the case of copying, and a new one in the case of deriving.

However, if you need to add a new traits object to an existing object, you will have to clone some abstract object that represents "being a traits object". Such abstract objects that model an intrinsic property of the system are called prototypes in Slate, and they roughly correspond to abstract classes in class hierarchies.

When Slate gets sufficiently advanced to support a meta-object protocol, the Slate object system will be described in terms of itself (as is the case of Smalltalk systems).

Namespaces

Each object is made accessible by its name, and each name, in turn, is defined within a scope: local variables have a meaning only inside the block they belong to; slots have a meaning inside the object that defines them, and so on... Now some objects have to be more globally accessible than that. Suppose you come up with a very useful application. Suppose I load your application in a Slate image, only to find out that some of the names you used for your objects are the same as some I have, but with a different and incompatible implementation.

Instead of requiring the names of objects to be unique across all possible Slate images there can be in the world :-), it is easier to restrict the meaning of the names for more global objects to a certain scope. That scope is called a namespace. I would then load your application inside a namespace specially created for it, so that I can still use it without incurring name clashes with any other part of the system.

A Namespace object is a Slate object whose primary use is to hold other objects as its slots. Since the scope of slots is restricted to the current object, all objects within the Namespace are only accessible from within that Namespace.

The idiomatic way to add a namespace is to use the #ensureNamespace: method. This method is defined on the Root object's traits, making it usable from anywhere in Slate.

Slate 2> ensureNamespace: #MyRoom.
(prototypes . globals . Mixins . Types .
        MyRoom . VM . traits )
Slate 3> MyRoom traits == Namespace traits.
True
Slate 4> 

A Namespace clone called MyRoom was added to the lobby. To fill a namespace, all you have to do is use the #addSlot:valued: method.

Slate 4> MyRoom addSlot: #bag valued: Bag newEmpty.
(bag . traits )
Slate 5> 

Trying to reach for the #bag slot directly from the lobby yields an error, since that slot is encapsulated within the MyRoom namespace.

Slate 5> bag.
The following condition was signaled:
The method #bag was not found for the following arguments:
{(prototypes . globals . Mixins . Types .
                MyRoom . VM . traits )}

The following restarts are available:
0)      Inspect a stack frame
1)      Abort evaluation of expression
2)      Quit Slate
Debug [0..2]: 1

Slate 6> 

The #bag slot can be accessed using the expression: MyRoom bag. This expression selects the accessor method for the #bag slot of the MyRoom object. This accessor was automatically created by the #addSlot:valued: method we used to create the slot.

Sometimes it is desirable to directly access all slots present in a namespace without having to reference it first. This can me accomplished by using the #ensureDelegatedNamespace: method, which behaves as #ensureNamespace: method, but also marks the new namespace as a delegation slot for the englobing object.

Slate 6> MyRoom ensureDelegatedNamespace: #Desk.
(traits . Desk . bag )
Slate 7> MyRoom Desk addSlot: #book valued: 'Dune'.
(book . traits )
Slate 8> MyRoom book.
'Dune'
Slate 9> 

In the last expression, the accessor #book was not found on the object MyRoom, and the method lookup algorithm forwarded the request to the most recently added delegate for MyRoom, namely the namespace Desk. The accessor was found there and executed. Since the usual lookup algorithm is used for namespace delegation, it is necessary to be extra careful when using delegated namespaces, since this destroys one level of name encapsulation.

Slate 9> MyRoom ensureDelegatedNamespace: #Table.
(Desk . bag . Table . traits )
Slate 10> MyRoom Table addSlot: #book valued: 'East of Eden'.
(book . traits )
Slate 11> MyRoom book.
'East of Eden'
Slate 12>