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.
|
Answer whether the argument is the same object as the receiver. |
|
Answer whether the argument is not the same object as the receiver. |
|
Answer whether the argument should be considered equal to the the receiver, even if they are not identical. |
|
Answer whether the argument should be considered different from the receiver. |
|
Answer whether the receiver is |
|
Answer whether the receiver is not |
|
Print the receiver on standard output. |
|
Print the receiver, then a newline, on standard output. |
|
Issue a run-time error message which includes |
|
Report to the user that a method specified in the superclass of the receiver should have been implemented in the receiver’s class. |
|
Answer whether the receiver’s class is the argument or a subclass of the argument, |
|
Answer whether the receiver’s class is exactly the argument, |
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 thanrecvObject = 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 writtenaClass
--- an object of theClass
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.
|
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. |
|
The receiver is a class; answer its superclass, or if it has no superclass, answer |
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
|
Evaluate the receiver and answer its value. |
|
Bind that |
|
|
|
|
|
Like |
|
Send |
|
Send |
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
|
If the receiver is true, evaluate |
|
If the receiver is false, evaluate |
|
If the receiver is true, evaluate |
|
If the receiver is false, evaluate |
|
Answer the conjunction of the receiver and the argument. |
|
Answer the disjunction of the receiver and the argument. |
|
Answer the complement of the receiver. |
|
Answer true if the receiver is equivalent to the argument. |
|
Answer true if the receiver is different from the argument (exclusive or). |
|
If the receiver is true, answer the value of the argument; otherwise, answer |
|
If the receiver is false, answer the value of the argument; otherwise, answer |
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
|
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
totrue
-
class of receiver is
True
-
does class
True
implementnot
? no! -
superclass of receiver is
Boolean
-
does class
Boolean
implementnot
? yes! -
method body is
(self ifTrue:ifFalse: {false} {true})
-
evaluate
(self ifTrue:ifFalse: {false} {true})
, whereself
istrue
-
send
ifTrue:ifFalse:
toself
/true
-
class of receiver is
True
--- important message sent toself
begins search at actual (at runtime) class of receiver, not the class in which the message send appears -
does class
True
implementifTrue:ifFalse:
? yes! -
method body is
(trueBlock valueString)
-
evaluate
(trueBlock value)
wheretrueBlock
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 theBoolean
class that does not explicitly use thetrue
andfalse
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.