An elegant design conserves mechanisms. It combines a small number of primitives in various ways. When I first learned about this elegant bit of design in Smalltalk-80, I laughed with delight.
In Smalltalk, the primitives are “object” and “message”. That’s basically it – except for blocks, which we will see a little later. Behavior arises via objects sending messages to each other. In fact, Smalltalk doesn’t even need control structures in the language grammar. Objects and messages suffice. How does a language without control structures do anything useful? How can any conditional logic work?
The key is with the class hierarchy for Boolean
, True
, and False
.
In most languages, “boolean” is a primitive type that doesn’t have any behavior. True and false are values of the type boolean. Not so in Smalltalk. Boolean
is an abstract class that has two subclasses: True
and False
.
Boolean
defines selectors like ifTrue:
and ifTrue:ifFalse:
but does not implement them. Each parameter is a block: an object wrapping a chunk of behavior that can be invoked later. (Ruby also calls these blocks, but only allows one at the end of a parameter list.) In Smalltalk, arguments are interleaved with the words in the method selectors. Here’s an example from Squeak, a modern Smalltalk, in the Character
class:
isSeparator
"Answer whether the receiver is one of the separator characters--space,
cr, tab, line feed, or form feed."
| integerValue |
(integerValue := self asInteger) > 32 ifTrue: [ ^false ].
integerValue
caseOf: {
[ 32 "space" ] -> [ ^true ].
[ 9 "tab" ] -> [ ^true ].
[ 13 "cr"] -> [ ^true ].
[ 10 "line feed" ] -> [ ^true ].
[ 12 "form feed"] -> [ ^true ] }
otherwise: [ ^false ]
The first line just names the method. The quoted string is documentation visible in the class browser. | integerValue |
says this method uses one local variable. Then we get to the interesting line. (integerValue := self asInteger)
sends the asInteger
message to self
and assigns the result to integerValue
. The assignment returns the value which was assigned, an integer object. Next, the >
message is sent to the integer object, with 32
as a parameter. Yes, comparison “operators” are also just messages sent to objects. Every number is an object. The result of >
is an instance of Boolean
. So the paradoxical-seeming ifTrue: [ ^false ]
will be sent to whichever Boolean
was returned from >
. The caret just means “return” and false
is a literal that names the singleton instance of the class False
.
That’s a lot of messages in one short line of code. Thanks to the hard work of many brilliant programmers and quite a few transistor-doublings since 1980, it performs well today. There are also many tricks with pointer tagging and flyweight objects that make it reasonable to have numbers represented with objects.
Now we get to the punchline and the genius of Smalltalk’s little trio for Boolean logic. So True
implements ifTrue:
something like this:
ifTrue: trueBlock [
"We are true -- evaluate trueBlock"
<category: 'basic'>
^trueBlock value
]
(This sample from GNU Smalltalk. )
True
knows it is true, so it unconditionally evaluates the block. It won’t surprise you to see that False
implements ifTrue:
like this:
ifTrue: trueBlock [
"We are false -- answer nil"
<category: 'basic'>
^nil
]
All the other variants such as ifTrue:ifFalse
and ifFalse:
are implemented similarly. In fact and:
and or:
operate the same way.
The beautiful part about this is how a small number of features, used consistency and pervasively, combine to allow simplicity to emerge.
Control structures can be discarded in favor of objects sending messages and evaluating blocks. Polymorphism subsumes conditionality, but it only works if objects are pervasive. If Smalltalk had the same split between boxed numerics versus primitive numbers that Java uses, this wouldn’t work. Numbers must be objects. True and false must be objects, not primitive values or puns for distinguished values of uint8_t
.
Since I learned about Smalltalk’s elegant trio, I’ve tried to apply the same principle in my own designs. Maybe we can push an idea farther. Make it more pervasive. Get more mileage out of it. Represent some other behavior (like conditionality) with a more simpler but more general idea (like polymorphism.) Ask the question, “What if we made everything an X?” for some value of X.