Protocols

A Smalltalk term of art: a protocol is the collection of messages that an object can respond to and the expected behavior (including result) of the object when sent the message. The protocol of an object is determined by the class of which it is an instance.

When describing behaviors in Smalltalk, we say "answers" rather than "returns".

Object protocol

All objects in Smalltalk are instances of classes that are subclasses of the Object class, so all objects adhere to the Object protocol.

== anObject

Answer whether the argument is the same object as the receiver.

!== anObject

Answer whether the argument is not the same object as the receiver.

= anObject

Answer whether the argument should be considered equal to the the receiver, even if they are not identical.

!= anObject

Answer whether the argument should be considered different from the receiver.

isNil

Answer whether the receiver is nil.

notNil

Answer whether the receiver is not nil.

print

Print the receiver on standard output.

println

Print the receiver, then a newline, on standard output.

error: aSymbol

Issue a run-time error message which includes aSymbol.

subclassResponsibility

Report to the user that a method specified in the superclass of the receiver should have been implemented in the receiver’s class.

isKindOf: aClass

Answer whether the receiver’s class is the argument or a subclass of the argument, aClass.

isMemberOf: aClass

Answer whether the receiver’s class is exactly the argument, aClass.

Two things to note:

  • The receiver object (which is necessarily an object adhering to the protocol being described) is not mentioned in the "signature". That is, the protocol writes = anObject, rather than recvObject = anObject. Remember this when reading protocols and it seems like an argument is missing; the argument that seems to be missing is usually the implicit receiver.

  • A protocol typically specifies the "type" of arguments to messages in terms of classes/protocols. The isKindOf: message requires a class object argument, so the argument is written aClass --- an object of the Class class.

The nil object is provided by the initial basis; it is the sole instance of the UndefinedObject class and typically is used for a bad, missing, or uninitialized value. Sometimes it is used as the result value for a method that doesn’t have a meaningful result.

Class protocol

All class objects adhere to the Class protocol.

new

The receiver is a class; answer a new instance of that class. A class may override new, e.g., if it needs arguments to initialize a newly created instance.

superclass

The receiver is a class; answer its superclass, or if it has no superclass, answer nil.

See the textbook for additional methods of the Class protocol, mostly providing a limited form of class inspection and modification.

Blocks

A block is a special kind of object that represents the delayed evaluation of an expression. A block is similar to a lambda-expression in uScheme and the syntax of a block-expression is similar to that of lambda-expression:

  • In abstract syntax:

    Exp = ...
        | BLOCK of name list * exp list
  • In concrete syntax:

    (block (x1 x2 ... xn) e1 e2 ... em)
    
    { e1 e2 ... em }

    Note that { e1 e2 …​ em } is syntactic sugar for the parameterless block (block () e1 e2 …​ em).

A block-expression returns a block object (an object that is an instance of the class Block).

The body expressions e1, e2, …​, em are not evaluated when the block-expression is evaluated (just like the body expression of a lambda is not evaluated when the lambda-expression is evaluated).

When a block object is sent the value message (with no arguments), the value: message (with one argument), the value:value: message (with two arguments), etc., the body expressions e1, e2, …​, em are evaluated (just like when a closure is applied, the body is evaluated).

Repeat: you don’t "apply" a block; you "send a block a value message".

-> (val inc (block (n) (n + 1)))
<Block>
-> (inc value: 4)
5
-> (inc 4)
syntax error: found receiver inc with no message
-> (val printHi { ('Hello println) 0 nil })
<Block>
-> (printHi value)
Hello
nil
-> (printHi)
syntax error: found receiver printHi with no message

In addition to the formal parameters, the body expressions of a block-expression can mention any variables in scope. When the block is sent a value message, the body expressions are evaluated in the environment that was present when the block-expression was evaluated. (Again, very similar to closures in uScheme.)

-> (val i 0)
0
-> (val bumpi (block (x) (set i (i + x))))
<Block>
-> (bumpi value: 5)
5
-> i
5
-> (bumpi value: 10)
15
-> i
15

The real power of block-expressions is that when they are used in a method definition, then the variables in scope will include the local variables of the method, the formal parameters of the method, and the instance variables of the object. Particularly interesting is the treatment of the variable self --- although a block-expression yields an object, uses of self in the body expressions will refer to the receiver object of the method, not to the block itself. (Reread this paragraph again after considering some of the methods in the Collection class.)

Recall the 2nd Smalltalk slogan: Control structures are implemented by sending messages.

Control structures (e.g., conditionals, loops) are implemented as messages that take blocks as arguments.

  • conditionals are implemented as messages to Boolean objects

  • loops are implemented as messages to blocks (that answer a Boolean object)

Take a moment to appreciate this minimalist design. In most languages, like uScheme, if and while are dedicated syntax in the language with special evaluation rules in the semantics/interpreter. But, in Smalltalk, if and while are not built into the semantics/interpreter --- they are simply provided by the initial basis and encouraged by idiomatic programming.

Conditionals

We use "continuation-passing style" to implement conditions. (Continuations were described in the uScheme chapter of the textbook, although they were not discussed in lecture.)

Send the ifTrue:ifFalse: message to a Boolean object along with two (nullary) blocks; if the receiver Boolean object is true, then the value message (with no arguments) will be sent to the first argument block; if the receiver Boolean object is false, then the value message (with no arguments) will be sent to the second argument block.

-> (val a 5)
5
-> (val b 10)
10
-> ((a < b) ifTrue:ifFalse: (block () a) (block () b))
5
-> ((a < b) ifTrue:ifFalse: {a} {b})  (1)
5
-> ((a < b) ifTrue:ifFalse: a b)      (2)
Run-time error: SmallInteger does not understand message value
1 Note the use of the { e1 …​ em } syntactic sugar for writing nullary (argumentless) blocks.
2 NOTE: This is one of the most common errors in Smalltalk programming!! Always double-check ifTrue:ifFalse: message sends: the receiver must be a Boolean object and the two arguments must be (nullary) blocks.

More on the Boolean class(es) below.

Loops

We also use "continuation-passing style" to implement loops.

Send the whileTrue: message to a (nullary) block (which answers a Boolean object) along with a (nullary) block; the value message is sent to the receiver; if it answers true, then the value message will be sent to the argument block and repeat; if it answers false, then answer nil.

-> (val factAns nil)
nil
-> (val fact (block (n)
               (set factAns 1)
               ({(1 <= n)} whileTrue:          (1)
                 {(set factAns (factAns * n))  (1)
                  (set n (n - 1))})            (1)
               factAns))
<Block>
-> (fact value: 5)
120
-> (fact value: 10)
3628800
-> (fact value: 5)
120
-> (val fact (block (n)
               (set factAns 1)
               ((1 <= n) whileTrue:            (2)
                 {(set factAns (factAns * n))
                  (set n (n - 1))})
               factAns))
<Block>
-> (fact value: 5)
Run-time error: True does not understand message whileTrue:
1 Note how these blocks access the global variable factAns and the outer block’s formal parameter n.
2 NOTE: This is another common error in Smalltalk programming!! Always double-check whileTrue: message sends: the receiver must be a (nullary) block (that answers a Boolean object) and the argument must be a (nullary) block.

Block protocol

value

Evaluate the receiver and answer its value.

value: anArgument

Bind that anArgument to the receiver’s formal parameter, evaluate the body of the receiver, and answer the result.

value:value: arg1 arg2

value:value:value: arg1 arg2 arg3

value:value:value:value: arg1 arg2 arg3 arg4

Like value:, but with two, three, or four arguments.

whileTrue: bodyBlock

Send value to the receiver, and if the response is true, send value to bodyBlock and repeat. When the receiver responds false, answer nil.

whileFalse: bodyBlock

Send value to the receiver, and if the response is false, send value to bodyBlock and repeat. When the receiver responds true, answer nil.

Blocks vs. Procs in Ruby

The Ruby programming language includes features very similar to Smalltalk’s blocks.

In Ruby, objects of the Proc class are equivalent to Smalltalk’s blocks --- a first-class object that acts like a closure. Instances of the Proc class respond to the call message (much like instances of the Block class respond to value messages in Smalltalk).

Ruby also has things called "blocks", but they are not first-class objects. At a message send, in addition to arguments, one can give a block, which can only be used by the receiving method via the yield keyword. But, there are mechanisms for converting an explicit Proc to an implicit block and for converting an implicit block to an explicit Proc.

Booleans

The Boolean class hierarchy is a exemplar of object-oriented design. Unfortunately, booleans in many object-oriented languages (e.g., Java) are primitive types, rather than objects.

The key to the Boolean class hierarchy is a rich collection of methods that are all implemented in the abstract superclass Boolean in terms of one critical "abstract" method, which is implemented in the two concrete subclasses True and False.

Boolean protocol

ifTrue:ifFalse: trueBlock falseBlock

If the receiver is true, evaluate trueBlock, otherwise evaluate falseBlock.

ifFalse:ifTrue: falseBlock trueBlock

If the receiver is false, evaluate falseBlock, otherwise evaluate trueBlock.

ifTrue: trueBlock

If the receiver is true, evaluate trueBlock, otherwise answer nil.

ifFalse: falseBlock

If the receiver is false, evaluate falseBlock, otherwise answer nil.

& aBoolean

Answer the conjunction of the receiver and the argument.

| aBoolean

Answer the disjunction of the receiver and the argument.

not

Answer the complement of the receiver.

eqv: aBoolean

Answer true if the receiver is equivalent to the argument.

xor: aBoolean

Answer true if the receiver is different from the argument (exclusive or).

and: alternativeBlock

If the receiver is true, answer the value of the argument; otherwise, answer false (short-circuit conjunction).

or: alternativeBlock

If the receiver is false, answer the value of the argument; otherwise, answer true (short-circuit disjunction).

The Boolean protocol includes the "full" conditional ifTrue:ifFalse: described above and it’s counterpart ifFalse:ifTrue:, the "partial" conditionals ifTrue: and ifFalse:, the usual boolean operations (and, or, not, eqv, xor), and short-circuiting conjunction and disjunction (which take a block as an argument, so as to only evaluate when required). All of the "work" of the Boolean protocol is handled by the ifTrue:ifFalse: method --- all other methods are defined in terms of ifTrue:ifFalse:.

Th "trick" to the Boolean class hierarchy is start with an abstract Boolean class that implements all methods in terms of ifTrue:iFalse:, create two concrete subclasses for True and False, and create two global instance objects true and false.

Boolean implementation

(class Boolean
   [subclass-of Object]
   ; no instance variables

   (method ifTrue:ifFalse: (trueBlock falseBlock)
     (self subclassResponsibility))                       (1)

   (method ifFalse:ifTrue: (falseBlock trueBlock)
     (self ifTrue:ifFalse: trueBlock falseBlock))
   (method ifTrue:  (trueBlock)
     (self ifTrue:ifFalse: trueBlock {}))                 (2)
   (method ifFalse: (falseBlock)
     (self ifTrue:ifFalse: {} falseBlock))                (2)
    
   (method & (aBoolean)
     (self ifTrue:ifFalse: {aBoolean} {self}))            (3)
   (method | (aBoolean)
     (self ifTrue:ifFalse: {self} {aBoolean}))            (3)
   (method eqv: (aBoolean)
     (self ifTrue:ifFalse: {aBoolean} {(aBoolean not)}))  (3)
   (method xor: (aBoolean)
     (self ifTrue:ifFalse: {(not aBoolean)} {aBoolean}))  (3)
  
   (method not ()
     (self ifTrue:ifFalse: {false} {true}))               (4)

   (method and: (alternativeBlock)
     (self ifTrue:ifFalse: alternativeBlock {self}))
   (method or:  (alternativeBlock)
     (self ifTrue:ifFalse: {self} alternativeBlock))
)

(class True                                               (5)
   [subclass-of Boolean]
   ; no instance variables
   (method ifTrue:ifFalse: (trueBlock falseBlock)
     (trueBlock value))
)
; the singleton instance of True
(val true (True new))

(class False                                              (5)
   [subclass-of Boolean]
   ; no instance variables
   (method ifTrue:ifFalse: (trueBlock falseBlock)
     (falseBlock value))
)
; the singleton instance of False
(val false (False new))
1 Because Smalltalk has no compile-time type checking, there is no facility to mark a class/method abstract and rely on the compiler to check that a concrete subclass implements all of the abstract methods of the superclass. Instead, the "abstract" method in the superclass is implemented by sending itself the subclassResponsibility message that issues an appropriate error message.
2 The "partial" conditionals are implemented by passing the argument block as appropriate to in a message send of ifTrue:ifFalse: to self. Note that the "empty" block {} answers the nil object when evaluated (as does an "empty" begin-expression).

Because the trueBlock and falseBlock arguments are instances of the Block class, they are passed "directly" to ifTrue:ifFalse: and not as block-expression. Nonetheless, if one wanted to be very careful and always use explicit block-expressions for every message send of ifTrue:ifFalse:, then we could write them as follows:

   (method ifTrue:  (trueBlock)
     (self ifTrue:ifFalse: {(trueBlock value)} {}))
   (method ifFalse: (falseBlock)
     (self ifTrue:ifFalse: {} {(falseBlock value)}))
3 Where possible, the boolean operations are expressed in terms of self and the argument aBoolean, rather than with explicit reference to the true and false objects.
4 The not method is an exception to the above. (But, see question below.) Like Impcore, uSmalltalk does not capture the global environment at class definitions; instead, when evaluating a variable, the global environment as it exists at that point in execution is used. So, like Impcore, uSmalltalk can use a global variable in a method implementation before that global variable is defined. Thus, not can reference the true and false objects before they are defined.
5 All of the "magic" happens in the True and False subclasses, which provide appropriate implementations of the ifTrue:ifFalse: method.

Example

Consider the evaluation of (true not):

  • evaluate (true not)

  • send not to true

  • class of receiver is True

  • does class True implement not? no!

  • superclass of receiver is Boolean

  • does class Boolean implement not? yes!

  • method body is (self ifTrue:ifFalse: {false} {true})

  • evaluate (self ifTrue:ifFalse: {false} {true}), where self is true

  • send ifTrue:ifFalse: to self/true

  • class of receiver is True --- important message sent to self begins search at actual (at runtime) class of receiver, not the class in which the message send appears

  • does class True implement ifTrue:ifFalse:? yes!

  • method body is (trueBlock valueString)

  • evaluate (trueBlock value) where trueBlock is {false}

  • answer false

Exercises

  • Consider the following definitions:

    (val mytrue (True new))
    (true == mytrue)
    (true = mytrue)
    (true eqv: mytrue)

    What does each "comparision" answer? Are you satisfied with the answers? Could you change the Boolean class definition to obtain more satisfactory answers?

  • Can you provide an alternative implementation of the not method in the Boolean class that does not explicitly use the true and false objects?

    Hint: not must answer an object that behaves like the negation of the receiver object (and need not actually be the global value that represents the negation of the receiver object).

    Trace out the evaluation of ((true not) not) with this implementation.

Alternative Boolean Implementation

The textbook actually presents a different implementation of booleans, where all methods of the Boolean class are abstract and the True and False sub-classes provide appropriate ("optimized") implementations of all of the methods.

Boolean implementation

(class Boolean
   [subclass-of Object]
   ; no instance variables

   (method ifTrue:ifFalse: (trueBlock falseBlock)
     (self subclassResponsibility))

   (method ifFalse:ifTrue: (falseBlock trueBlock)
     (self subclassResponsibility))
   (method ifTrue:  (trueBlock)
     (self subclassResponsibility))
   (method ifFalse: (falseBlock)
     (self subclassResponsibility))
    
   (method & (aBoolean)
     (self subclassResponsibility))
   (method | (aBoolean)
     (self subclassResponsibility))
   (method eqv: (aBoolean)
     (self subclassResponsibility))
   (method xor: (aBoolean)
     (self subclassResponsibility))
  
   (method not ()
     (self subclassResponsibility))

   (method and: (alternativeBlock)
     (self subclassResponsibility))
   (method or:  (alternativeBlock)
     (self subclassResponsibility))
)

(class True
   [subclass-of Boolean]
   ; no instance variables
   (method ifTrue:ifFalse: (trueBlock  falseBlock) (trueBlock value))
   (method ifFalse:ifTrue: (falseBlock trueBlock)  (trueBlock value))
   (method ifTrue:         (trueBlock)             (trueBlock value))
   (method ifFalse:        (falseBlock)            nil)

   (method &    (aBoolean) aBoolean)
   (method |    (aBoolean) self)
   (method eqv: (aBoolean) aBoolean)
   (method xor: (aBoolean) (aBoolean not))
   (method not  ()         false)

   (method and: (alternativeBlock) (alternativeBlock value))
   (method or:  (alternativeBlock) self)
)
; the singleton instance of True
(val true (True new))

(class False
   [subclass-of Boolean]
   ; no instance variables
   (method ifTrue:ifFalse: (trueBlock  falseBlock) (falseBlock value))
   (method ifFalse:ifTrue: (falseBlock trueBlock)  (falseBlock value))
   (method ifTrue:         (trueBlock)             nil)
   (method ifFalse:        (falseBlock)            (falseBlock value))

   (method &    (aBoolean) self)
   (method |    (aBoolean) aBoolean)
   (method eqv: (aBoolean) (aBoolean not))
   (method xor: (aBoolean) aBoolean)
   (method not  ()         true)

   (method and: (alternativeBlock) self)
   (method or:  (alternativeBlock) (alternativeBlock value))
)
; the singleton instance of False
(val false (False new))

What are the advantages of this implementations? What are the drawbacks?

Acknowledgments

Portions of these notes based upon material by Norman Ramsey and Hossein Hojjat.