Stalking the Blue-eyed Smalltalk
"Know what's in it before you eat it," Ewell Gibbons used to say his daddy said. That's exactly how I feel about Smalltalk.
Smalltalk is a language, a data base, a development environment, a set of precepts, and a way of life for many of its users. It is a whole system, as I use that term in these readers. It is flawed, but far less so than most other such systems. Smalltalk arose at Xerox Parc in the early 1970s from roots in a language called Simula and from Alan Kay's Flex system. Smalltalk, and other object- oriented systems, are gaining popularity. The second annual conference on object-oriented programming systems, languages and applications (OOPSLA'87) drew over 1100 participants to Kissimmee Florida. An IEEE Committee is at work standardizing Smalltalk. Several books are out, and you can buy an impressive PC version called Smalltalk/V for only $99.95.
If you mention Smalltalk, you may find that people know that it is object-oriented and they may know that objects inherit properties of other objects, somehow. Also people might mention that messages are sent to objects. Unfortunately that is about all they know. I know, because I have asked people who really aught to have known. Up until this year I had only used Smalltalk a few times and never with any particular understanding or success. On the advise of Ken Burgett, I got a copy of Smalltalk/V and tried it out. It did some impressive things like:
- ran on both NPC and my Hyundai XT clone,
- used the mouse in reasonable ways,
- made full use of graphics and multiple-windows,
- had a comprehensive tutorial built in,
- and included a complete graphics editor with all its source code in Smalltalk.
But I still didn't really understand what was going on. Then I came across a book called "A Little Smalltalk" by Timothy Budd who, like me, wanted to know more about Smalltalk but was having trouble doing so. He is a professor, so he and his students learned by writing a version of Smalltalk as a class project. Then he wrote a book describing both Little Smalltalk itself and his implementation. Dr. Budd's book started to make sense to me then Smalltalk/V started to make more sense. This week I took one example from Prof. Budd's book and rewrote it for Smalltalk/V (which is very different from Little Smalltalk which is a terminal based Unix version.) In this reader I will describe this example from several points of view as a basis for additional readers on object-oriented issues.
Discrete event simulation is a modeling technique in which real world events are simulated repeatedly so that performance measures can be made. Random numbers are used to lend variety. For this example Budd and I picked an ice cream shop. At time zero our store has no profit, but we expect to make 17 cents per scoop sold. Customers arrive at random intervals of from one to three minutes and buy from one to three scoops. If we run our simulation how much profit will we make? This may seem like a rather simple minded simulation, and it is, but it will point out many aspects of Smalltalk. Believe me, this is going to be hard enough.
Smalltalk is about objects. Objects are, at the most primitive level, only machine registers such as bits and bytes. Arbitrarily complex objects can be constructed out of associations of these most primitive objects. These associations are called 'classes.' Hang on to the idea of class for a minute while we talk about variables.
Unlike traditional procedural-languages like Basic, C, Cobol, or Fortran; Smalltalk variables are nothing more than pointers to objects. In C we have variable types such as 'char' which holds an 8 bit byte or 'int' which holds 16 or 32 bit things (this is machine dependent.) If we say:
int i; ... i++;
for example, the compiler knows that we want to add 1 to the contents of the cell called 'i' which the compiler knows is an integer type of thing. We say, "i is type integer."
Smalltalk variables are only pointers to objects and as such they are all the same size. Procedural-language variables vary all over the map in size. Smalltalk objects take on many different sizes. It may begin to look like procedural- language variables are like objects, they are, but Smalltalk objects are nothing like procedural-language variables. In Smalltalk our little code fragment would be quite similar:
| i | .... i addOne
But what could this mean? Since i is just a pointer to some bits in memory how does the compiler know what 'addOne' means? The compiler doesn't. Smalltalk works differently. In Smalltalk terminology "the message 'addOne' is sent to object pointed to by i." But what could it mean to 'send a message' to some bits in memory? It doesn't mean anything in any Von Neumann way. What they mean is "execute code at selector 'addOne' for i's class." Remember objects only have meaning in the context of their class. In C syntax what we have is:
addOne ( &i; ); /* & means send the address of i */
Sending a message is just like calling a procedure with, possibly, some arguments.
Objects, then, contain not only primitive computer stuff and pointers to other objects; but also a pointer to its own class. A class is just a collection of code and variables that define an object, or more exactly, the behavior of an object when it 'receives' a message.
For our example, we first think of discrete event simulation in some abstract way. Let's make a class of simulation objects! (Are we having fun yet?) Such a class of objects would have to know:
-- parameter -- -- variable name -- what time it is now currentTime what is the next event nextEvent when is the next event nextEventTime.
We would like this class of objects to tell us what time it is, we would like to tell it about events, and we would like it to know how to do the next event. Sounds kind of complicated? Here is all the necessary code (I use a simplified syntax for clarity):
Class LSsimulator "comments in double quotes" instanceVariables: 'currentTime nextEvent nextEventTime' time ^currentTime "return current time" addEvent: event at: eventTime "update variables from parameters event and eventTime" nextEvent := event. nextEventTime := eventTime " := is assignment" proceed "run the next event" currentTime := nextEventTime. self processEvent: nextEvent "remember self for later"
What actually happens depends on what kind of simulation we want to do, but this is the general form of a discrete event simulator. Now we need an ice cream store.
Naturally we make a class of IceCreamStore objects as a 'subclass' of our simulator. All Smalltalk classes are in a hierarchy, and this organization is central to Smalltalk's design. An IceCreamStore needs to know its profit so we set up a variable called 'profit' for an object which will eventually be an integer number of cents. (I left out floating point for now since it won't run on my NPC only my Hyundai.) The variable 'profit' is called an 'instance variable' because each instance of class IceCreamStore will need its own profit object. (One management team one focus.) currentTime, nextEvent, and nextEventTime are also instance variables. We need messages to setupStore, precessEvent, scheduleArrival of customers, and reportProfits. (All code is shown in figure 1.) Objects 'inherit' messages and variables from all 'super' classes. Here we get profit plus currentTime, nextEvent, and nextEventTime as variables in our IceCreamStore object, one set for each shop, if we have more than one. If we send a message to an IceCreamStore it will be answered (executed) as soon as it is found by a search of message selectors starting with those in class IceCreamStore, continuing with LSsimulation, and ending with the big class in the sky which is called Object in Smalltalk/V (to insure that terminology is as confusing as possible.) For example, if we send the message 'time' to IceCreamStore it will actually be answered in class LSsimulation by returning a pointer to object currentTime. This is everything there is to the lofty concept of inheritance.
-------------- LSsimulator instanceVariables: 'currentTime nextEvent nextEventTime ' addEvent: event at: eventTime "add an event" nextEvent := event. nextEventTime := eventTime proceed "do an event" currentTime := nextEventTime. self processEvent: nextEvent time "answer the current time" ^currentTime --------------- IceCreamStore instanceVariables: 'profit ' setupStore "build a new IceCream Store" 'Another Fine IceCream Store' p. profit := 0. currentTime := 0. self scheduleArrival. ^self processEvent: event "make a single event happen" ( 'at', (self time), ' minutes a Customer appears') p. profit := profit + (event numberOfScoops * 17). self scheduleArrival scheduleArrival "plan for the next customer" self addEvent: Customer new at: (self time + (3 randInteger)) reportProfits "answer how much we made" ('Profits are:', profit, ' cents today.') p. --------------- Customer numberOfScoops "a customer is no more than what he buys" | scoops | scoops := 3 randInteger. (' (s)he has', scoops, ' scoop(s)') p. ^scoops Figure 1. IceCreamStore Class Descriptions
Finally, as a subclass of class IceCreamStore, we build a class of customers. As I have set up this example, customer objects need no variables, and a shop schedules its customers instead of customers scheduling themselves. In fact, all a customer needs to do is state the number of scoops wanted. Actually, we only need class customer to be a placeholder for an event. It is a pointer to an instance of Customer that will fill variable nextEvent in an instance of IceCreamStore.
If you are lost you are probably not alone, keep an open mind, and remember that it has taken me 12 years to get this far. Perhaps you'd like to take a break and get an ice cream. We are about half way. I don't mind a bit.
What we have so far doesn't actually do anything in itself. We need some kind of driver to simulate a store.
Running Smalltalk/V on a 640K 8MHz 8088 with a Hercules 720 x 348 graphics display is a little like driving a fully featured Cadillac with a Volkswagon engine. All the stuff is there, its just a little sluggish. When I first used Smalltalk at Xerox Parc many years ago, it was so slow we used the terms 'majestic' and 'glacial' to describe performance. All in all, Smalltalk/V useable even on my Hyundai.
Figure 2 shows a screen dump just after I ran our simulator for 10 simulated minutes. Actually it takes about 20 seconds. These windows are Xerox windows. By means of pop- up menus (pop-up menus come up right by the cursor) you can move and size any window. Windows overlap; and can be brought to the top by simply clicking. On the left is 'System Transcript' showing 6 simulation events. At top right is a most interesting window called 'Class Hierarchy Browser.' This window has several 'panes.' At top right is a list of all classes of the whole Smalltalk/V system and in the pane below is some source text. We see our simulator with class IceCreamStore selected in light grey. You scroll to see others. To the left is a list of all message selectors available for class IceCreamStore. This pane is currently scrolled to its last entry: setupStore. Just below you see two panes which toggle on an off for 'instance' and 'class.' It turns out that messages can be sent to classes themselves or to instances of classes. But let's not bother with this too much now.
Smalltalk/V is implemented in Smalltalk/V and not only is source code for our simulator here and available; but also all source code for the entire system. I can modify any part of the system at any time, add classes below any previous defined class, add methods for any class, and generally do anything I consider appropriate.
Browsers are wonderful. But I save additional comment about them for some future reader.
Below my browser is a workspace window called IceCream. It is just a place to write things. Here is where I typed code to drive the simulator. To run this code, I select it with either mouse or cursor keys, pop up a menu, and select 'do it.'
To simulate our IceCreamStore we use the following code:
| store |
to set up a variable 'store' to point to an instance of IceCreamStore. Then:
store := (IceCreamStore new) setupStore.
sends message 'new' to class IceCreamStore which answers a pointer (not named) to a blank object which is an instance of class IceCreamStore. Next setupStore is sent to that pointer which does necessary setup (code in Figure 1) and schedules the first event before returning a pointer to itself to save in 'store.' Store now points to an initialized object which an instance of class IceCreamStore. Message selector new, by the way, was inherited from well above our simulator. Now the big event:
[store time < 10] whileTrue: [store proceed]
does all the work. Things in square brackets are called blocks. Blocks can have any number of statements in them, they can be nested, and their value is the value of the last statement executed. Consider the first block a moment. I find it easy to understand by just looking at it: "as long as the time in this simulation is less than 10 say true otherwise false." That is just what it does mean but actually what happens is:
time is sent to store which answers a pointer to currentTime then message '<' with argument pointer to instance of integer (current value 10) is sent to currentTime which answers a pointer to an object of class boolean containing either true or false as appropriate which is the result of this little block.
Now I think you might begin to understand why it is so slow, or conversely, how amazing it is that it runs at all.
Take it for granted that whileTrue: causes the message proceed to be sent to store as long as the previous block stays true. Now lets delve into proceed.
Proceed is defined in LSsimulator. It sets currentTime to nextEventTime. Then it says:
self precessEvent: nextEvent
which is quite a marvelous thing. First 'self' is a pseudo variable which always represent the object currently receiving a message, 'store' in our case. Remember we sent proceed to store. Store points to an instance of IceCreamStore but it inherits 'proceed' from LSsimulator which really doesn't know anything about IceCreamStores. The phrase 'self processEvent' allows us to say something about things which don't actually exist when LSsimulator is written and which will be valid for any sort of simulation no matter if it is an IceCreamShop or a PostOfficeLine. LSsimulator is, however, counting on each simulation implementing a 'processEvent' suitable for itself. If not, Smalltalk/V will say 'message not understood' and give us some rather nice debugging tools.
So 'store' (self) receives 'processEvent:' with argument 'nextEvent.' processEvent puts out the text string you see in Transcript showing 'self time' as the number of minutes into simulation. Profit is computed but a key thing happens in:
which sends 'numberOfScoops' to 'event' which is actually a pointer to a Customer. In numberOfScoops we get the second print line on Transcript, and return a random number of scoops. Then we jump off to scheduleArrival to plan for another customer.
'scheduleArrival' builds new nextEvent and nextEventTime objects by using 'addEvent' in LSsimulator. 'Customer new' builds a pointer to an object of class Customer just like 'IceCreamStore new' did in the driver. This pointer gets saved into nextEvent. A random number will be added to currentTime to form nextEventTime. This cycle repeats as long as currentTime is less than 10.
Finally, when currentTime exceeds 10 we move on to:
which is duck soup for you by now.
I hope I have given you the idea that a hell of a lot of objects are built and discarded during this process because they were.
Objects in Smalltalk are allocated dynamically as needed. Storage allocation is a major issue with Smalltalk and with any implementation a great deal of care is needed to predict use and optimize memory management design.
Figure 3 is sort of a snapshot of our simulator at some arbitrary point during execution. This figure should help to reinforce comments about pointers made above. It also shows, in a small way, how objects are discarded. As new objects of class Customer are created to build nextEvents old ones are discarded. Each object contains a reference count which measures the total number of pointers assigned to it. As pointers are removed, this count is decremented. So in the expression:
currentTime := nextEventTime
the reference count in the object pointed to by currentTime must be decremented before the value given by nextEventTime is moved into currentTime.
Objects with 0 counts are free and can be collected by something called a 'garbage collector.' There is no natural time for a single processor machine to collect garbage and the system usually waits until it runs completely out of space to do so. This can result in some unpredictable delays during execution. Other nasty effects, too numerous to mention, can also happen.
A discarded Customer is shown with reference count 0 at the bottom of figure 3.
Smalltalk works on objects which are defined by a hierarchy of classes.
Subordinate objects inherit the behavior of superior ones.
Pointers are the only kind of variable manipulated by Smalltalk code.
You are not meant to deduce if Smalltalk is or isn't a good thing by this reader.
by Robert L. Belleville ([email protected])
An RLB Reader (written: 04/29/88, published: 11/13/96, src: WR1V10.HTM)
Copyright Robert L. Belleville 1996. All rights reserved.