Typed slots for Pharo

Pharo is a highly-dynamic language providing an advanced reflexive API. In particular, the instance variables of a class are reified as Slots objects. This feature allows one to manipulate instance variables (or slots) of a class as any object in the system.

In this blogpost, I will explain how to extend slots to allow some kind of type-checking on the value they hold.

A reflexive duck, for the sake of illustration. (source)

Slots

The class comment of Slot class begins as follow:

I’m a meta-object for accessing a slot in an Object.
I define a protocol to read (#read:) and to write (#write:to:) values.

What is interesting about slots is that they provide hooks allowing to perform custom actions when the value it holds is either read (#read:) or written (#write:to:). They will be useful for our implementation of TypedSlot.

But first, let’s take a look at slots of Color to see how it works usually.

Color slots. "{#rgb => InstanceVariableSlot. #cachedDepth => InstanceVariableSlot. #cachedBitPattern => InstanceVariableSlot. #alpha => InstanceVariableSlot}"

Color uses default slots of the system named InstanceVariableSlot. It has 4 slots named #rgb, #cacheDepth, #cachedBitPattern and #alpha.

It is possible to ask Color for a particular slot using #slotNamed: method.

slot := Color slotNamed: #rgb.

A slot knows its name:

slot name. "#rgb"

And we can use it to read #rgb values of various Color instances.

slot read: Color black. "0"
slot read: Color white. "1073741823"
slot read: Color blue. "1023"
slot read: Color green. "1047552"

Or to write them (do not do that in practice, it breaks encapsulation and since Color white is cached it will do a lot of mess in the colors of the system, turning everything white to be black ;-)):

colorWeModifyWithSlot := Color white.
slot read: colorWeModifyWithSlot. “1073741823”
slot write: 0 to: colorWeModifyWithSlot.
slot read: colorWeModifyWithSlot. “0”

Now that we have a better idea of what are slots, let’s look at how they work. To do that, we’ll take a concrete example: the implementation of TypedSlot. This new kind of slot will allow to verify the type of the object one attempt to store in the slot is the one expected.

Typed slots

First, let’s write some unit tests to describe the behaviour we expect from these slots. First, we create a mock class that will be used in our tests:

Object subclass: #MockObjectForTypedSlotUsingClass
slots: { #testSlot => TypedSlot type: Integer }
classVariables: { }
package: 'TypedSlot-Class-Tests'

The above class holds a TypedSlot named #testSlot that will only allow one to store Integer objects.

We can now write our unit tests. First we create a subclass of TestCase:

TestCase subclass: #TypedSlotClassDescriptionTest
slots: { }
classVariables: { }
package: 'TypedSlot-Class-Tests'

Then, we implement #testWriteTo which describes the behaviour we expect from TypedSlot>>#write:to: method.

TypedSlotClassDescriptionTest>>testWriteTo
| testSlot mockObject |
testSlot := MockObjectForTypedSlotUsingClass slotNamed: #testSlot.
mockObject := MockObjectForTypedSlotUsingClass new.

self shouldnt: [ testSlot write: 1 to: mockObject ] raise: TypeViolation.
self assert: (testSlot read: mockObject) equals: 1.

self shouldnt: [ testSlot write: nil to: mockObject ] raise: TypeViolation.
self assert: (testSlot read: mockObject) equals: nil.

self should: [ testSlot write: 'string' to: mockObject ] raise: TypeViolation.
self assert: (testSlot read: mockObject) equals: nil.

Back to the mock class creation, the syntax to create a TypedSlot with Integer as type is the following:

#testSlot => TypedSlot type: Integer

In fact, we would like our implementation to be extensible. So we will do it in a way that any object can be provided as argument of #type: as long as it is able to check if a particular object instance can or can not be stored in the TypedSlot.

This check will be performed by #checkObjectType: that will be implemented by an type object. In the case of classes, we expect the behaviour of this method to answer the requirements described by the following test:

TypedSlotClassDescriptionTest>>testCheckObjectType
self shouldnt: [ Integer checkObjectType: 1 ] raise: TypeViolation.
self shouldnt: [ Integer checkObjectType: -1 ] raise: TypeViolation.
self shouldnt: [ Fraction checkObjectType: 1/2 ] raise: TypeViolation.
self shouldnt: [ Fraction checkObjectType: 0.5s02 ] raise: TypeViolation.

self
should: [ Integer checkObjectType: 'string' ]
raise: TypeViolation
withExceptionDo: [ :typeViolation |
self assert: typeViolation expectedType equals: Integer.
self assert: typeViolation objectAttemptedToBeWritten equals: 'string' ].

self
should: [ ScaledDecimal checkObjectType: 1/2 ]
raise: TypeViolation
withExceptionDo: [ :typeViolation |
self assert: typeViolation expectedType equals: ScaledDecimal.
self assert: typeViolation objectAttemptedToBeWritten equals: 1/2 ].

Alright, we now have tests to describe what we expect from the implementation we are going to achieve. So, let’s get started.

A specific error to model type-violation

First of all, we need to create a special error that describes that a type violation occured:

Error subclass: #TypeViolation
slots: { #expectedType. #objectAttemptedToBeWritten }
classVariables: { }
package: 'TypedSlot-Errors'

We create accessors and mutators for #expectedType #objectAttemptedToBeWritten instance variables and we create an class-side method to make TypeViolation instance creation easy:

TypeViolation>>expectedType: aType objectAttemptedToBeWritten: object
^ self new
expectedType: aType;
objectAttemptedToBeWritten: object;
yourself

Delegating type-checking to ClassDescription

Second, we will implement #checkObjectType: in instance side of ClassDescription so we ensure that both classes and meta-classes can check the type of an object and thus be used for type-checking.

ClassDescription>>checkObjectType: anObject
(anObject isKindOf: self)
ifFalse: [ (TypeViolation expectedType: self objectAttemptedToBeWritten: anObject) signal ]

As you can see in the code above, the implementation is quite straightforward. If anObject provided as argument is not a kind of the ClassDescription, a TypeViolation is raised. Else nothing happens, the object can be stored in the slot. At this point, #testCheckObjectType should pass.

Implementing TypedSlot

Finally, we get into the implementation of TypedSlot. It will inherit from IndexedSlot which provides default behaviour required for custom-implementation slot.

IndexedSlot subclass: #TypedSlot
slots: { #type }
classVariables: { }
package: 'TypedSlot-Core'

Our TypedSlot holds a #type instance variable for which we create an accessor and a mutator. Furthermore, we initialise the type to be Object by default:

TypedSlot>>initialize
super initialize.
self type: Object

Since TypedSlot holds state, we override #definitionString, #= and #hash accordingly to the documentation of Slot.

To implement #definitionString, we will create a #definitionStringOn: method that prints the definition of the slot on the stream provided as parameter and we will call this method from #definitionString.

TypedSlot>>definitionStringOn: aStream
aStream
store: self name;
nextPutAll: ' => ';
nextPutAll: self class name;
nextPutAll: ' type: '.
self type printOn: aStream
TypedSlot>>definitionString
^ String streamContents: [ :stream |
self definitionStringOn: stream ]

Additionally, we override #hasSameDefinitionAs: to take into account the #definitionString (it is not taken into account in superclass implementation).

TypedSlot>>hasSameDefinitionAs: otherSlot
^ (super hasSameDefinitionAs: otherSlot) 
and: [ self definitionString = otherSlot definitionString ]

#= and #hash methods simple take into account the additional state of the slot:

TypedSlot>>= anObject
^ super = anObject and: [ self type = anObject type ]
TypedSlot>>hash
^ super hash bitXor: self type hash

The next method we implement is #checkTypeOfValue:. Its purpose is to delegate type-checking to the object stored in the #type instance variable. Additionally, this method ensure that if the value to be stored is nil, the type checking is not performed. This behaviour ensures it is always possible to set the slot value to nil.

TypedSlot>>checkTypeOfValue: newValue
newValue ifNil: [ ^ self ].

type checkObjectType: newValue

We can finally override the #write:to: hook to perform type-checking before actually storing the new value.

TypedSlot>>write: newValue to: anObject
self checkTypeOfValue: newValue.

^ super write: newValue to: anObject

That’s it, TypedSlot is now fully implemented and all tests pass.

Optimising read

The TypedSlot is now implemented and it uses reflectivity for both writing and reading (which means that both #write:to: and #read: will be used allowing us to define custom behaviour). When writing the slot, it verifies the type of the object to be written. On the other hand, when reading the slot nothing needs to be done. Thus, it is not needed (nor wanted) to read the value held by the slot reflectively. Doing such read has an overhead leading to a slower access to the value than when using default slots.

To optimise read operation performed by TypedSlot, we can use the same trick as the one used by InstanceVariableSlot>>#emitValue:. This method has the responsibility to generate the byte-code located at places where the slot is read in the code. In the IndexedSlot, the byte-code is such that one can override #read: hook to perform custom actions. Since we do not need to do such thing, we can override #emitValue: to generate byte-code that will only read the value of the slot, without performing #read: hook. Such byte-code can be generated as follow:

TypedSlot>>emitValue: methodBuilder
methodBuilder pushInstVar: index.

Conclusion

This blogpost illustrates how easy it is to extend Pharo language. In particular, we implemented a TypedSlot which ensures an object with the wrong type can not be written. For those who want to go further, a more advanced implementation can be found here. In this implementation more features are implemented such as using Traits or BlockClosures as “type”. I even did some experiment to implement interfaces (similar to Java’s concept of interface) for Pharo.

What is interesting with the small experiment in this blogpost is that it opens another perspective: the need for slot composition. Indeed, one would like to be able to add type-checking to any kind of slot. This would allow, for example, to provide type-checking for slots that perform further actions if the type is accepted. There are already some experiments with slot composition so this feature will come into Pharo at some point.

Acknowledgments

Thanks to Marcus Denker for reviewing this blogpost.