Disclaimer:
This is an unsolicited contribution and not an official component of the environment.
All errors and inconsistencies are my responsibility and I do not assume any legal responsibility for any errors.
The text was originally written for VisualWorks by Ivan Tomek, and intended to be read in a VisualWorks workspace. The transcription into HTML and inclusion of Smalltalk/X differences was done by Claus Gittinger.
I use the following colors to emphasize different types of information:
Blue for executable code and program-related information.
Red for environment commands appearing, for example, "Print it" in popup menus.
Black for all other text. Important words are underlined or bold.
Green for naviagion links.
The original text was used in a workspace, and editable - this html transcription is readonly; however, you can copy text into a workspace and try a modified version of the example there (try the right button menu, while the mouse pointer is over some example text). Also, if you read this document inside the Smalltalk/X help-viewer, you can click on most code examples and see the result in the info area at the bottom of the document reader window.
- Part 1: Why Smalltalk?
- Part 2: Objects and messages
- Part 3: Objects and classes
- Part 4: Defining and editing classes and methods
- Part 5: Essential classes and methods
- Part 6: Other important classes
- Part 7: Creating graphical user interfaces
- Part 8: Developing a Smalltalk application
- Part 9: What next
- References
In the following sections, I will introduce enough Smalltalk so that you can start developing simple applications and, more importantly, explore further easily.
Everything in Smalltalk, including numbers, strings, windows, the compiler,
and the parts of the interactive development environment is an object.
Objects communicate by sending messages to one another and every message returns an object as answer (return value).
Everything in Smalltalk happens by sending a message to an object.
In a Smalltalk expression, such as
the receiver object that executes the message (here the string 'abc') always comes first,
the message (here reverse) follows. Think of a Smalltalk expression as an order to the receiver to execute a message.
Just as you might say in English
'abc' reverse
or
dog bark
the expression 'abc' reverse is "an order to the receiver to 'reverse' itself".
car go
The following are examples of valid Smalltalk expressions;
the text surrounded by double quotes " are comments and ignored by Smalltalk.
3 squared "Receiver is the integer object 3; the message is squared.
It is an order to the object 3 to calculate and return its square."
'abc' asUppercase "Receiver is the string object 'abc', the message is asUppercase.
It is an order to object 'abc' to calculate and return its uppercase form."
To execute these examples one-line-at-a-time and see the results, proceed as follows:
200 factorial "Receiver is the integer object 200, the message is factorial.
Read it as an order to 200 to calculate and return its
factorial: 200*199*198*..*1."
In a VisualWorks Workspace or if the text has been typed or copied into a Smalltalk/X Workspace:
Do not try to execute the three lines together - Smalltalk would treat them as one big but inconsistent expression. More on this later.
By the way, you can type and evaluate Smalltalk expressions in any text space, not just the Workspace. However, I recommend that you open a new workspace for your additional experiments by executing the command sequence Tools -> Workspace in the main Launcher window (select Tools in the menu and then click the Workspace command).
There are only three kinds of messages in Smalltalk: unary, binary, and keyword messages. The difference between them is in the form of the name of the message (its selector) and the number of arguments that that they expect.
Unary messages consist of a single 'word' (properly called an identifier); they don't have any arguments.
Evaluate the following three examples line-by-line with "Print it".
3 negated "Receiver is 3, message selector is called negated"
27 squared "Receiver is 27, selector is squared"
Binary messages use one or two (ST/X: or three) special characters such as + - / \ @ , & | and @ for selector,
and the selector is followed by one argument.
'black fox' asUppercase "Receiver is 'black fox', selector is asUppercase"
Examples are
3 + 5 "Receiver is the integer number 3, selector is +, argument is the integer object 5"
'abc' , 'xyz' "Receiver is 'abc', selector is , and argument is the string object 'xyz'"
'one' -> 'eins' "Receiver is 'one', selector is -> and argument is the string object 'eins'"
50 @ 100 "Receiver is 50, selector is @ and argument is 100"
Keyword messages allow any number of arguments.
The selector consists of one or more keywords (a keyword is an identifier followed by a colon),
and each keyword is followed by argument:
17 // 5 "Receiver is 17, selector is //, argument is the integer 5"
3 between: 5 and: 10 "Receiver is 3, message selector is between:and: and arguments are 5 and 10"
$d between: $a and:$r "Receiver is character d, message selector is between:and: and arguments are characters a and r"
3 raisedTo: 17 "Receiver 3, message selector is raisedTo: and argument is 17"
Dialog request: 'Your name, please' initialAnswer: 'John'
"Receiver is 'class' object Dialog (more later), selector is request:initialAnswer:"
The following expression uses a single keyword message with six keywords.
Its selector is choose:fromList:values:lines:cancel:for:
'aLongWord' chopTo: 5 "Receiver is string 'aLongWord', selector is chopTo:"
I show it using multi-line formatting commonly used for keyword messages with multiple arguments,
but you can type it all on one line or in any other style because Smalltalk does not pay attention to 'white space'.
Evaluate the expression by selecting all seven lines together and executing "Print it"
Note that identifiers (names of unary messages and keywords) start with a lowercase letter and contain only letters and digits.
They have unlimited length and are case sensitive, so that asUppercase and asUpperCase are different.
To verify this, try evaluating:
Dialog
choose: 'Which line do you want?'
fromList: #('first' 'second' 'third' 'fourth')
values: #(1 2 3 4)
lines: 8
cancel: [#noChoice]
for: Dialog defaultParentWindow
This is wrong because there is no message called asUpperCase (remember - its called: asUppercase),
and Smalltalk will open an error dialog.
You can respond with cancel (abort) to terminate execution and correct the code, or with correct it.
In this case, Smalltalk will offer one or more similalrly sounding selectors and if you select one,
it will correct the mistake and continue evaluation.
The correction mechanism usually succeeds when you make a small typing mistake but larger mistakes must be corrected by hand.
'abc' asUpperCase "will trigger an error"
Every message returns an object and you can thus use an expression as the receiver of another message.
Evaluate the following lines one by one with "Print it".
5 factorial squared "Integer 120 - the result of 5 factorial
- is the receiver of message squared"
'abc' asUppercase reverse "The result of 'abc' asUppercase
is the receiver of message reverse"
Note that you can even select and evaluate parts of the above 'chained' expressions.
As an example, you can execute only the
13 factorial sqrt truncated even "Interpretation: Calculate 13 factorial, then its square,
truncate to an integer, and tell me whether it is even"
5 factorial
or the 13 factorial sqrt
parts of the lines above if you wish.
This is sometimes very useful.
For the same reason, expressions can also be used as arguments as in
3 raisedTo: 5 squared "The result of 5 squared is the argument of the keyword message raisedTo:
so this is equivalent to 3 raisedTo: 25"
'the number is: ', 5 printString "The result of 5 printString is the argument of the binary concatenation
message , and equivalent to 'the number is: ', '5' "
The following combination where both the receiver and the arguments are calculated is also valid
Dialog confirm: 'abc' asText allBold "The argument of confirm: is the result of sending two consecutive messages to 'abc' "
but the following formulation is more readable and thus preferable:
5 factorial between: 3 squared and: 3 * 5 * 9
Smalltalk programmers routinely chain messages, sometimes to even greater depth, but the facility should not be overused at the expense of readability.
(5 factorial) between: (3 squared) and: (3 * 5 * 9)
Evaluation of combined messages obeys the following simple rule:
As an example, check that
is equivalent to
3 + 2 raisedTo: 2 squared + 7 "squared first (unary), + next (binary), raisedTo: last (keyword)"
These simple rules apply to everything; there are no other precedences - even where you might expect them. So, for example,
(3+2) raisedTo: ((2 squared) + 7) "Parentheses first, unary messages next, binary then, keyword last."
is a sequence of binary messages and is evaluated left-to-right. It is thus equivalent to
5 + 3 * 4
which is not what you might have expected.
To avoid unpleasant surprises, use parentheses for more complicated expressions,
even when they are not necessary.
Adding them does not affect evaluation speed but improves readability.
(5 + 3) * 4
5 + (3 * 4)
Program code usually consists of more than a single expression. I will call a sequence of expressions a code fragment (some people call it a script) and its components are called statements.
Consecutive Smalltalk statements are separated by periods, just as English sentences.
As an example,
is a small (and meaningless) code fragment consisting of two statements.
If you execute it by selecting both lines and using "Print it",
you will get the result returned by the last expression.
3 squared.
5 factorial
As another example, the following two lines clear the 'Transcript' - the bottom part of the main Launcher window - and display 'Hello world'
Execute this code fragment with
"Do it" and then examine the Launcher window.
(You can execute it with "Print it" as well, but here we are interested in the effect of the two lines rather than the object returned by the last expression.
To see the difference, try "Print it" as well.)
Transcript clear. "Note the period separating this statement from the next."
Transcript show: 'Hello world!'
Notes: - The word Transcript is capitalized because it denotes a different kind of entity than the objects we used so far. I will explain this later. - Execution of "Print it" consists of "Do it" followed by the display of the result of the last message.
If a sequence of consecutive statements has the same receiver,
you can use cascading to eliminate the need to retype the receiver.
As an example, when you execute the following fragment with "Do it"
you will see that it has the same effect as the 'cascaded' form
Transcript clear. "Note that this and the following statements have the same receiver Transcript."
Transcript show: 'Hello world!'.
Transcript cr.
Transcript show: 'How are you?'
The semicolon indicates that the next message is sent to the same receiver as the current message.
Note that although each consecutive message in a cascade goes to the same object,
the state of that object may be changing as the messages are evaluated.
Transcript clear; "Note the semicolon separating the 'cascaded' messages instead of a period separating statements."
show: 'Hello world!'; "Receiver is not repeated."
cr;
show: 'How are you?'
In this example, the receiver of all messages is Transcript.
The original Transcript may have contained text, but after the clear message, it is empty -
its state has changed.
You can always write code without using cascading - cascading is just a shorthand to save typing and possibly make code easier to read. Some people think that code is easier to read without cascading and avoid it. I (Ivan) think that when you use proper indenting, cascading improves readability.
After evaluating a message, Smalltalk automatically discards the returned object unless you assign it to a variable, or unless the object is referenced by another object that still exists. (This is called automatic garbage collection.) So if you need to keep an object for later use, assign it to a variable, a named reference to an object that can be used to refer to it later in the program. All variables in Smalltalk must be 'declared' before the first statement and the declaration lists the names of all required variables but does not specify their type. Here is an example:
Evaluate this code fragment with "Do it" and examine the Transcript to see the result.
| price tax total | "Declaration: Variable names separated by spaces."
price := (Dialog request: 'Please enter price' initialAnswer: '100') asNumber.
tax := (Dialog request: 'Please enter tax %' initialAnswer: '20') asNumber.
total := price + (price * tax / 100).
Transcript clear;
show: 'price: ', price printString; cr;
show: 'tax: ', tax printString; cr;
show: 'total: ', total printString; cr.
The reason why variables don't have a declared type is that they are just pointers to objects, essentially addresses of memory locations holding the representation of the object to which they are 'bound'. As a consequence, a variable can point to one object in one part of a fragment and another in another part of the same fragment, but this is considered poor programming style.
The variables introduced in this code fragment are called temporary variables and their scope (range in which they can be used by the program) and lifetime are limited to the code fragment. When the code fragment is fully evaluated, they cease to exist and the objects that they point to are discarded - unless they are referenced by other objects. We will see other types of variables later.
The := is the assignment operator and it binds the result of the right hand side of the assignment statement to the variable on the left hand side.
Notice that in the original ST-80 and still today in Squeak Smalltalk, the assignment operator is the '_'-character, which is displayed as a backarrow ( <- ) in those systems' fonts. Most modern Smalltalks have changed to use ':=', but still support the old syntax. (In ST/X, there is an option in the settings dialog to enable this).
Real objects have properties: A person has a height, weight, name, address, and age,
and many of these properties change during a person's lifetime.
A car has a manufacturer, color, and mileage, and different cars usually have different values of these parameters.
A major justification of object-oriented languages such as Smalltalk is that they can be used to model
real-world objects and concepts.
As a result, Smalltalk objects may also have properties and their values may also change during their lifetimes.
As an example, every Borrower object representing a library patron in a library application may be characterized
by a first and a last name, an ID, and a list of borrowed books, but different Borrower objects probably have
different values of these properties.
Every Book object may have an author, a title, a publisher, a library number, and a borrower,
but different Book objects have different property values.
And every Fraction object is naturally characterized by its numerator and denominator.
To examine the properties of a Smalltalk object, use the Inspector tool.
Evaluate each of the following lines separately with "Inspect it" from the <operate> menu:
3 @ 5 "Returns a Point object as indicated in the label at the top of the Inspector window.
Its components are shown below."
Rectangle origin: (3 @ 5) corner: (25@30) "Returns a Rectangle object."
The "Inspect it"-Command first executes "Do it" and then opens an inspector on the object returned by the last message.
You can also inspect an object by sending it the inspect message as in
3 / 5 "Returns a Fraction."
As you might guess, the inspect message is the essence of the "Inspect it" command.
(Q: what happens if you use the "Inspect it"-Command in the previous example, instead of "Do it" ?)
(3 @ 5) inspect "Evaluate with Do it"
If an object has properties they are, of course, again objects because everything is an object. The rectangle returned by the second line is a composite object - its components are objects (points - ST/X: numbers) with their own properties. To see their structure, select the line with its name and execute "Dive" (ST/X: "Follow") in the <operate> menu of the list. To return to the previous Inspector level, execute "Back" in the <operate> menu or use the arrow button at the top of the Inspector.
All fractions are objects are objects of the same kind and share the same definition,
which says that a fraction has a numerator and a denominator and that it understands certain messages.
The only difference between individual fractions is that they may have different numerator and denominator values.
Similarly all point objects share the same definition and so do all text objects.
The definition of all objects of the same kind is gathered in a special kind of object called a class.
Fractions are defined in class Fraction, points in class Point, and text objects in class Text.
You can think of a class as a 'blueprint' kind of object mainly used to create instances of the objects that it defines.
Class Fraction can create objects such as 3 / 5 and 4 / 7 and these two fractions are two instances of class Fraction.
Properties of an object are stored in its internal slots called instance variables
and each instance of a class has the same own instance variables with its own private values.
As an example, every Point has an x and a y coordinate.
Similarly, every Fraction has a numerator and a denominator - again each with its own private values.
The value of an instance variable of a given object may or may not change during its lifetime.
As an example, when a window object is moved on the screen or resized the instance variable that defines its
size and position changes its value.
Objects thus carry along their private properties but their definitions are in their defining class.
Note that Smalltalk class names always begin with a capital letter.
The questions related to the fact that classes are objects are related to the very interesting but advanced topic of metaprogramming.
I don't cover it in this introduction.
If you ever need to find the class of and object, send it the class
message as in
3 class "Execute with Print it or with Inspect it."
(3 @ 5) class
Exercise:
(Rectangle origin: (3 @ 5) corner: (25@30)) class
Note: When an exercise requires writing and evaluating code, I recommend using a separate Workspace.
Smalltalk's tool for viewing, editing, creating, and destroying class definitions is called the Browser.
Smalltalk provides several kinds of browsers and you can open them from the Launcher
or by sending them a message such as
SmalltalkWorkbench browseClass: Fraction "that is the VisualWorks version"
Smalltalk browseClass: Fraction "that is the Smalltalk/X version"
When you execute this expression with "Do it",
you will get a Browser on class Fraction.
Smalltalk browseInClass: Fraction "another Smalltalk/X version"
Notice, that most Smalltalk systems offer various browsers and corresponding
startup messages; for example, there might be browsers showing a single class only,
multiple classes, inheritance trees etc.
A much more common and easier way to open the Browser via the Launchers menu or the corresponding Launcher button.
The Browser is a very powerful tool that allows you to see and edit all code including, for example, the code that defines the Browser itself and this Workspace. It allows you to change anything you want - even the rules of the language - at your own risk. The Browser is the tool that Smalltalk programmers use to create new applications. We will see how later.
Exercises:
Smalltalk provides several browsers, each suitable for a particular task. We will describe the Class Browser (in the following simply the Browser) because it is the simplest and easiest to use. The other browsers have a similar structure but may have more or fewer components.
A Browser window has four list panes at the top and a text view at the bottom. The main roles and uses of these views are as follows:
Note to VisualWorks users:
If all your source code does not show up in your Browser, you probably don't have the correct 'home' setting.
The home setting is the file directory at which VisualWorks starts looking for files.
To check the setting and correct it if necessary, go to the Launcher,
open the File command in the menu bar, and click "Set VisualWorks Home".
The dialog explains that you should set this to the main directory containing your VisualWorks files,
such as "C:\vwnc5i.1\" or "/usr/local/vwnc5i.4".
Exercises:
Magnitude-Numbers
?
Rectangle
, Date
, String
?
Rectangle
and what is the comment of the class?
Rectangle
? Which ones on the class side?
corner:
" in class Rectangle
and which protocol is it in?
(Hint; VW users: Use command "Find Method.." in the protocol view to find a method in the selected class if you don't know its protocol.
ST/X users: either use the "Find Response to"-command in the Find menu,
or select "*all*" in the protocol list to see all methods)
Spline
.
The complete expression to evaluate the example message is given in the comment at the beginning of the method.
Object allSubclasses
select: [:aClass |
aClass class organization categories includes: #examples]
As the code is organized slightly differently in ST/X, the examples
are found via:
Object allSubclasses
select: [:aClass |
aClass class implements:#examples]
Use the information obtained from this expression to find and evaluate other example methods.
(The expression above is an example of the use of Smalltalk's reflectivity.
One of its most important uses is the implementation of tools such as the Browser, the Inspector, and the Debugger.)
ST/X users: the browsers protocol list menu contains a function "Spawn Protocols Matching..." where a protocols name such as "examples" can be entered. This allows browsing all methods which are found in a particular protocol.
Because classes are objects, they understand messages. These messages are listed on the 'class side' of the class definition and are called class messages. (Messages on the 'instance side' are called instance messages.) To view them in the Browser toggle the class/instance button which is found below the class or protocol list.
Because classes are used mainly to create instances, most class messages are instance creation messages.
Almost every class inherits (to be explained) and can execute message new to create an uninitialized instance of itself with nil as the value of all its instance variables.
(The nil-Object is the single instance of class UndefinedObject and represents an object that 'does not have a value'.)
As an example, evaluate the following with "Inspect it" and examine the instance variables of the returned object:
In addition to understanding new, many classes define their own specialized instance creation messages that create initialized instances.
Rectangle new "Message new is a class message - its receiver is the Rectangle-class."
As an example, inspect individually the following lines:
Circle center: 23 @ 12 radius: 15 "center:radius: is a class message - its receiver is class Circle. It returns an instance of Circle."
Time now "now is a class message - its receiver is class Time.
The result is an instance of Time in the current time zone.
You can change the time zone via the File -> Settings command sequence in the Launcher."
Rectangle fromUser "Lets you draw a Rectangle on the screen and returns the resulting Rectangle object."
How can you recognize that
Date today "Read the comment of class Date to understand the meaning of its instance variables."
center:radius:
, now
, fromUser
, and today
are class messages?
If you are looking at an expression, the answer is that the type of a message can be determined from the nature of its receiver.
Receivers of class messages are classes and class names start with a capital letter and this gives a strong hint.
There are a few exceptions to this rule -
as an example, Transcript is not a class although it begins with a capital letter.
This is because capitalized names denote 'shared objects' and classes are only one of several kinds of shared objects.
More on this later.
Exercise
Here is an example that combines a class message with an instance message:
It is not uncommon for a class message and an instance message to have the same name.
Date today previous:#Monday "today is a class message (receiver is class Date) and returns an instance of Date;
previous: is thus an instance message because its receiver is the object Date today, an instance of Date."
As an example, the message 'initialize' is defined on both class and instance sides of several classes.
This does not create any problems because the nature of the receiver determines which definition should be used
- when the receiver is a class, the class-side method is evaluated,
when the receiver is an instance, the instance-side definition is used.
Some class messages do not create instances but return information related to the class. As an example, use the Inspector to check the result of
Time millisecondClockValue "Does not return a Time object - returns a SmallInteger."
Window platformName "Does not return a Window object - returns a String."
Still other class messages initialize the class itself or perform various other functions such as execute examples.
Filename volumes "Does not return a Filename object - returns a collection of strings."
Note that although most objects are created by sending an instance creation message to a class,
some of the most common objects can be created as 'literal' objects without sending a creation message.
Several examples are
32 "An integer number literal."
41.3 "A floating-point number literal."
'abc' "A String literal."
$x "A Character literal - letter x."
true "The single instance of class True."
false "The single instance of class False."
Think of literals as being objects which are created by the compiler.
nil "The single instance of class UndefinedObject."
Exercise:
Dialog
.
Browse the definitions
and execute the example code enclosed in comment brackets at the beginning of each of them with "Print it".
Smalltalk classes are related - every class (except one, as you will see) has exactly one superclass and inherits its class and instance methods and variables.
In other words, a class has all instance variables of its superclass (plus its own) and understands all its messages (plus its own).
As an example, the definition of class Fraction extends the definition of its superclass - class Number.
Fraction thus understands all methods defined in Number (plus its own) and its instances have all its instance variables (plus its own).
(In fact, class Number does not have any instance variables so Fraction does not inherit any.)
When you draw a diagram of classes and their superclasses in the form of a family tree, you will get Smalltalk's class tree.
The class on the top is class Object and this class is the only one that does not have a superclass.
If A is the superclass of B, and B is the superclass of C, C inherits from B, including everything that B inherited from A. C thus inherits everything from B and A. Inheritance is thus transitive and all classes ultimately inherit all behavior defined in class Object. As a consequence, if you want to define a message that every object should understand, define it in class Object. Modifying 'base clases' such as Object is not very common but it is possible.
To find the superclass of a class and its complete class hierarchy with all superclasses and subclasses, select the class in the Browser and choose "Hierarchy" in the View command ("ST/X: Class Hierarchy" in the View menu). VisualWorks Note: you must not select a method protocol if you want to see the hierarchy.
You can also ask a class what is its superclass as in
Message superclass is another example of Smalltalk's reflectivity, and the reason why the Browser can display the class hierarchy.
Fraction superclass
Exercises:
SmallInteger
, Date
, Rectangle
, Object
?
Number
.
(Note: These and related methods are defined in class Behavior.)
SmallInteger
, Date
, Rectangle
, Object
.
Object allSubclasses size
to find how many subclasses class Object has.
The result depends on how many parcels (packages) you loaded and how many classes you added or deleted.
An ostrich is a bird but it does not fly. In other words, ostrich redefines one of the behaviors that it inherits from its 'bird superclass'. A similar feat can be achieved in Smalltalk by overriding an inherited method definition by simply writing a new definition of the method in the class that should execute the message differently. Any class may redefine any of its inherited methods but cannot remove any inherited method or any of its inherited instance variables.
As an example of overriding, class Fraction
inherits the definition of =
but redefines it because equality has a special meaning for fractions.
(Two fractions are equal if they satisfy a certain arithmetic formula relating their numerators and denominators.)
This definition overrides the inherited definition and when you ask a fraction whether it is equal to another fraction, it executes its own definition of equality rather than the inherited one.
This is because when you send a message to an object, its evaluation starts with a search for its definition in its class.
If the class contains the definition, the definition is evaluated, otherwise the search for the definition continues in the superclass, and so on,
until a definition is found and executed, or the top of the hierarchy is reached and the definition is not found.
If the definition of the method is not found,
Smalltalk sends message doesNotUnderstand:
to the original receiver,
and this method by default creates an 'exception' object that indicates that (by default) opens an Exception window.
(The default behaviors can be overridden.)
To see an example of an exception created when a message sent to an object is not found in the path from the class of the receiver to the top of the hierarchy in class Object, evaluate
with "Do it".
You will get an Exception window because neither the class of the receiver (SmallInteger) nor any of its superclasses understand asUppercase, which is defined in class Character on a branch from Object that does not lead to integers.
Click "Terminate" (VisualWorks) (ST/X: "Abort") or in the Exception window to abort the evaluation.
We will see later how you can handle exceptions programmatically.
13 asUppercase
Exercises:
=
method that Fraction
inherits?
asUppercase
, fromUser
, <
, and printOn:
using the "Browse -> Implementors of..." command in the Launcher.
Which of these definitions override inherited methods?
True new "Class True 'forbids' message new"
Inheritance is great for avoding duplication of code. (Duplication is bad not only because it means extra work, but because it is dangerous - if you change something and forget to change even one of the copies, your application may fail to work.) As an example, if you need classes to define classes SavingAccount, CheckingAccount, and SpecialSavingAccount, many of the methods (such as deposit and withdraw) and many of the instance varibales (such as accountNumber, owner, balance) are probably needed in all three classes. To avoid this duplication, define Account as a superclass of these three classes, and include all the shared instance variables and methods in it. The three subclasses will not have to redefine them, unless some of the methods have different behavior.
Our application will never instantiate Account it does not use the general concept of an Account; it assumes only instances of the three subclasses. This is why Account is called an abstract class whereas SavingAccount, CheckingAccount, and SpecialSavingAccount are called concerete - they are designed to be instantiated. Smalltalks class hierarchy contains many abstract superclasses and class Object at the top of the hierarchy is itself abstract.
Note that there is nothing in Smalltalk that marks a class as abstract - the distinction is only a design feature in the mind of the designer and there is nothing to prevent the user from treating it as concrete.
As an example, inspect
It does create an instance even though you would very rarely want to do that.
Object new
(But: Yes, there are real uses for this; for example, as a unique token.)
Some abstract classes take precautions against creating instances as a protection from misuse. As an example, class ArithmeticValue redefines new as
Don't attempt to evaluate this code, it's not a code fragment but a method definition. As a consequence of this definition, executing
new
"Numbers should be created only through arithmetic operations."
^self shouldNotImplement "The special word self refers to the receiver of the message,
in this case class ArithmeticValue or one of its subclasses."
produces an exception (Notice: Not in ST/X).
ArithmeticValue new
Since ArithmeticValue is the superclass of all number classes, they all inherit this definition and refuse to execute the new message.
As an example,
also opens an Exception window (Also: Not in ST/X).
Fraction new
One of the typical features of abstract classes is that they contain methods that are redefined in all their subclasses.
These methods are defined essentially as templates, markers that serve as reminders that the concrete subclass must define them.
As an example, all numbers are supposed to understand multiplication, but most number classes implement it in a special way.
Class ArithmeticValue thus 'implements' multiplication as
If you defined a new subclass of ArithmeticValue, forgot to define message *,
and tried to multiply your new number, you would get an Exception window telling you that the class was supposed to implement this message but didn't.
This is the result of message
* aNumber
^self subclassResponsibility
subclassResponsibility
.
Exercises
subclassResponsibility
and explain why they are needed.
Smalltalk programs consist of classes with methods and this part shows you how to create and edit them. This section shows how to create a method. Because we start with methods, my example will use an existing class and add a new method to it or modify an existing method.
To define a new imethod
To modify an existing method, use the same procedure but edit the existing text instead of retyping it.
As an example, assume that we want to define an instance method to calculate the cube of a number.
The method will be used as in
or
15 cubed
Clearly, the message should be understood by all types of numbers,
just like the
1.5 cubed
squeared
-method.
The logical class to put it in is thus where the squared
method is defined, which is instance protocol mathematical functions in class ArithmeticValue.
The definition (explained below) will be as follows:
Type (or copy and past) the text into the code view in the
cubed
^ self * self * self
ArithmeticValue
class
and "mathematical functions
" protocol on the instance side and click "Accept" in the <operate> menu.
If you have not made any mistakes, this will compile the code and the protocol list will now display the selector cubed and the method will be added to the library.
To make sure that the new method is saved in the 'image file' when you quit Smalltalk,
use "File -> Save" in the launcher either now or later, or save on exit.
If you exit Smalltalk without saving, all work done since the last save is partially lost because the image file doesn't change automatically.
The 'changes file' (accessed via the ChangesBrowsr) allows you to recover the code, but the procedure is a bit more complicated - consult the On-line Help for details.
Test that the method works, for example by evaluating
3 cubed
and
-3 cubed
Notes:
(3.55) cubed
ArithmeticValue
, the superclass of all numbers. This is what abstract classes are for.
self
" in the body of the method refers to the receiver. As an example, in "3 cubed", self is 3, in "3.14 cubed" the receiver is 3.14, etc.
^
" in the definition means 'execute the following expression and exit from this method returning the result of the expression from this method'.
Exercises:
inc
that returns its receiver incremented by 1. As an example, 34 inc
returns 35.
isPalindrome
that returns true if its receiver string spells the same in reverse.
As an example, 'aba' isPalindrome
should return true,
but 'abs' isPalindrome
should return false.
(Hints: Use method reverse (ST/X: reversed) and don't forget the return operator.)
+*
" that returns the sum of the receiver and the argument,
all multiplied by the argument.
As an example "3 +* 5
" returns "(3 + 5) * 5" or "40".
smallerThan:orGreaterThan:
that returns true or false under obvious circumstances.
(Hint: This method is the logical negation of message between:and:
,
and the logical negation message is not
.)
implies:
that returns true for any combination of receiver true and false,
except when the receiver is true and the argument is false.
As an example true inplies: true
returns true,
but true implies: false
returns false.
(Hints: Write a subclassResponsibility definition in class Boolean,
and override it in class False and in class True. Don't forget the return operator.)
The method that you defined or a code fragment that you wrote
may not work the first time.
If your error is that you are sending a message to the wrong object,
for example asUppercase to a number as in
Smalltalk opens an Exception window that can be used to open the Debugger
to see what is wrong.
You can then correct the code and accept it in the Debugger as you would in the Browser
(using the "Accept" command),
possibly edit the data (two inspectors at the bottom of the Debugger),
and continue execution.
Or you can "terminate" (ST/X: abort) execution and start all over.
3 asUppercase "Try it"
If the program executes but the result is not as expected,
the error is in the logic of the code.
The first thing that you might want to do is to read your code and see if you can correct it.
If you don't see the problem but think that you know its approximate location,
or a place where you might start searching for the problem,
you can insert a breakpoint (usually a self halt
statement).
When execution reaches this point,
Smalltalk will open an Exception window and then the Debugger.
(halt
is defined in Object
and you can thus send
it to any receiver such as self halt
, nil halt
or 3 halt
.)
You can then continue executing the code step by step and find and correct the problem.
As an example of the use of a breakpoint,
evaluate the following faulty program and correct the mistake in the Debugger.
When you add a breakpoint at the indicated point, Smalltalk will open an Exception window saying
'Halt encountered'.
Open the Debugger with "Debug",
select 'unbound method' in the list at the top (it refers to the code from this workspace),
and continue executing it using either "Step" (to walk over the next message)
or "Send" (to enter the definition of the next message).
When you find the problem, correct it directly in the Debugger and "Accept" the change.
| price tax total |
price := (Dialog request: 'Please enter price' initialAnswer: '100') asNumber.
tax := (Dialog request: 'Please enter price %' initialAnswer: '10') asNumber.
"Insert self halt here to open an exception window."
total := price + (price * tax / 10).
Transcript clear;
show: 'price: ', price printString; cr;
show: 'tax: ', tax printString; cr;
show: 'total: ', total printString
The inspectors at the bottom of the Debugger window show the instance variables of the receiver (left) and temporary variables and message arguments (right), and you can change their values by selecting the variable, entering a new value, and accepting it with "Accept". You can then continue executing the code in the Debugger making any corrections you want, or exit from the Debugger and proceed with execution (command "Proceed" in the <operate> menu in the stack view at the top of the Debugger). You can also terminate execution and return to the Workspace or the Browser. When you have corrected the mistake, remove the breakpoints.
Smalltalk programmers depend on the Debugger so much that some actually develop code with it.
To be able to create classes, you must first understand the concept of a namespace. As you already know, classes are grouped into categories. In a similar but completely independent perspective, classes are also collected in namespaces. Whereas a category is just an organizing principle with no effect on run-time behavior, namespaces have profound effect on execution.
To understand why some Smalltalks have namespaces, consider a familiar analogy.
My office phone number is 585-1467.
If you call me from within my area, that's all you need.
However, if you are in Toronto or in San Francisco, this number is either unassigned or belongs to another person and to reach me, you must put my area prefix 902 in front of the seven-digit code.
And if you want to call me from Europe, you must put even more digits in front of that to select North America.
The 'area code' prefix of a phone number thus partitions phone numbers into unambiguous sets - one for Ontario, one for Montana, etc. - and allows the basic seven-digit codes to be assigned to many different people across North America with no confusion.
The idea of namespaces implements exactly the same principle for Smalltalk classes.
A namespace is simply a way to get around the serious limitation of earlier versions of Smalltalk that required that every class have a unique name.
This meant that code from different sources could only be combined if the sources did not define clases with the same class.
If I wrote a library catalog program with a class called Book
and loaded the On-line Help parcel (package),
my Book class could clash with the Book
class that Smalltalk uses in its On-line Help.
In fact, one of the Book
classes would overwrite and destroy the other and one application would stop working.
Namespaces partition class names and make it possible to name classes in one namespace without any regard for class names in other namespaces. Within one namespace, class names must be unique, but two different namespaces may contain classes with identical names without any conflict. As a consequence, if I put my Book class into my own namespace, Smalltalk doesn't mind that there is another Book class in the another namespace and will not confuse one for the other because the other class is invisible to my namespace - unless I 'import' the other namespace into it.
Namespaces are organized in a tree-like structure with namespace Root at the very top, the namespace Smalltalk underneath, and other namespaces below Smalltalk.
(ST/X: no Root here; Smalltalk is a TOP namespace, among others)
To see all namespaces now in your image, open the System Browser from the Launcher and select its NameSpaces mode of display.
The structure of the namespace tree is relevant mainly for accessing a name in a different namespace and for defining your own namespaces:
Should you need to access class Y in namespace X contained in namespace Smalltalk,
you can use the dot notation, as in: Smalltalk.X.Y
(ST/X: two colons instead of a dot, as in Smalltalk::X::Y
;
or enable dot notation via a compiler switch).
(The mechanism is designed so that you can leave the root namespace(s) out of these expressions.)
In VisualWorks, you can import the required namespace or class by specifying it in the import: argument of the namespace definition message in the browser.
(In ST/X, there is no import path; namespaces always only import the global Smalltalk namespace)
As you see, the concept of namespaces is very important, but I am not going to explore it further except for giving a simple example of class creation, and refer you to On-line Help for details.
Exercises:
To define a new class, you must know into which namespace and category to put it. If the namespace does not yet exist, you can create it using the System Browser. The next decision is to select the superclass of the new class. Because a subclass specializes (extends) the properties and functionality of its superclass, the superclass should be a class that serves a more general purpose than the new class. As an example, Vehicle would be a reasonable superclass for Car if your application deals with several kinds of vehicles and each of them requires its own class, and Account is a good superclass of SavingsAccount if you have several kinds of accounts. If you can't think of a suitable existing superclass , make your class a subclass of Object.
After deciding these basics, you must decide on instance variables and write and "Accept" the definition of the class in a browser. The next step should be adding a comment to the class. Command View -> Comment from the Browser's menu bar displays a comment template, edit it as appropriate and "Accept". Finally, create the necessary instance and class protocols and define methods. We will now demonstrate the procedure on a simple example.
Example (VisualWorks specific):
Our goal is to create class Name
with superclass Object
in the Smalltalk.Test
namespace
(ST/X: Smalltalk::Test
),
and category "Examples
".
The class will have instance variables firstName (a String), middleName (a Character), and lastName (a String).
Its protocols will include instance creation and instance variable accessing. Our class is a simple data holder class, not a very good example from the point of view of class design, but almost the simplest example of class definition.
There is no namespace called Test
within namespace Smalltalk so you must create it.
Open a System Browser, select namespace Smalltalk,
and execute commands "Add" (from the menu bar) and then "Namespace".
This displays the template
Edit it as follows, noting that I deleted the underlined word above:
Smalltalk defineNameSpace: #NameOfPool
private: false
imports: '
OtherNameSpace.*
private Smalltalk.*
'
category: 'As yet unclassified'
The imports: keyword gives our namespace access to all classes in namespace Smalltalk which, in turn, provides access to other very important namespaces.
Smalltalk defineNameSpace: #Tests
private: false
imports: '
private Smalltalk.*
'
category: 'As yet unclassified'
We can now create the new class in the new namespace.
To do this, execute commands "Class -> Add class" from the menu bar or from the <operate> menu of the second list view of the System Browser,
and select fixed size. This will display the template
I again underlined the parts that you must edit.
Note that the text is, in fact, a message to your selected namespace - you can read it as a command to the namespace
Smalltalk.Test defineClass: #NameOfClass
superclass: #{NameOfSuperclass}
indexedType: #none
private: false
instanceVariableNames: 'instVarName1 instVarName2'
classInstanceVariableNames: ''
imports: ''
category: 'As yet unclassified'
Smalltalk.Test
to create a new class called Name
.
(Because it is an ordinary message, you could use it even inside a program, thus creating a new class from a running program.
The same, of course, holds for the creation of a namespace.)
Edit the text by modifying the class name and instance variables as follows:
and click "Accept" from the <operate> menu.
malltalk adds the Core. part in front of Object, creates the class, adds it to the library, and displays it in the Browser.
Smalltalk.Tests defineClass: #Name
superclass: #{Core.Object}
indexedType: #none
private: false
instanceVariableNames: 'firstName middleInitial lastName '
classInstanceVariableNames: ''
imports: ''
category: 'Examples'
The next step is to write the class comment. With the new class selected, select Comment in the menu bar View command and edit the template as follows:
Instances of this class represent simple person names.
Instance Variables: firstName <String> middleInitial <Character> lastName <String>The comment is, of course, only for documentation and has no run-time effect. It is, however, poor programming practice to neglect it because comments are very useful for understanding an unfamiliar class.
It is now time to write methods.
I will start with instance creation because I want to test everything as soon as possible and I can't test any instance methods without reasonable instances.
I could, of course, create instances with the new message and then assign values to instance variables by a sequence of cascaded 'setter messages' as in
asssuming that I have already defined
Tests.Name new "ST/X: Tests::Name"
firstName: 'John';
middleInitial: $C;
lastName: 'LeCarre'
firstName:
, middleInitial:
, and lastName:
.
However, I will assume that when the application runs, it gets name properties from a dialog window and so a Name is always created with known components.
Given this background, I decided on the following instance creation pattern:
The new message
Tests.Name "ST/X: Tests::Name"
firstName: 'John'
middleInitial: $C
lastName: 'LeCarre'
firstName:middleInitial:lastName:
is a class message because its receiver is class Name. What should the message do? It must create a new instance of Name, send it an instance message to initialize the instance variables, and return the resulting initialized object. The definition is thus
Note the naming of arguments:
firstName: string1 middleInitial: aCharacter lastName: string2
^ self new
firstName: string1 "Instance message - to be written."
middleInitial: aCharacter
lastName: string2
Smalltalk programmers prefer to use names that suggest the class of the argument (such as aCharacter - indicating an instance of Character) because the keywords clearly identify its role.
This style helps the future user of the method to use proper arguments when sending the message.
To define the method, create a new class-side protocol instance creation and define this message in this protocol by typing (or pasting) it over the method template text.
There is no need for a method comment because the purpose and logic of the method are obvious. Click "Format" and "Accept" and the method appears in the protocol list.
If you now tried to evaluate the Name-creation expression above, you would get an exception because you have not defined the instance message used inside our definition (try it).
To define the instance-side method, go to the instance side of the Browser.
Method firstName:middleInitial:lastName:
simply assigns argument values to the instance variables and its definition is
Put this code into instance-side initialize-release protocol,
click "Format" and "Accept", and test by evaluating
firstName: string1 middleInitial: aCharacter lastName: string2
firstName := string1.
middleInitial := aCharacter.
lastName := string2
with "Inspect it" to check that the new methods produce the expected result. ("Print it" does not produce anything interesting until I redefine the operation of printString by defining a new printOn: method. Try it.)
Tests.Name "ST/X: Tests::Name"
firstName: 'John'
middleInitial: $C
lastName: 'LeCarre'
Exercises:
Name
by adding 'getters' such as firstName (returns the value of firstName).
Test them by creating a Name object and using the getters to print a description of the object in the Transcript.
printOn: aStream
message to make it possible to get Name descriptions such as
printString
.
Book
in namespace Tests
and category "Examples
".
Book will have instance variables title (a String), author (a Name), and year (an Integer).
It will have an initializing instance creation message and getters for all instance variables.
To complete this overview of foundations, I will now explain two new types of variables, less common in simple applications. They are class instance variables and shared variables (ST/X: class variables).
Because classes are objects, they can have their own instance variables and although they are not really special, they have a special name. They are called class instance variables. There is really nothing unusual about them and they follow all rules of instance variables. In particular, if a class defines its class instance variables, they are inherited by its subclasses, and each of these subclasses has its own private values independent of other classes. As an example, if class Animal defines a class instance variable called Sound, then its subclasses Dog and Cat inherit Sound, but its value in Dog may be 'bark', while the value in Cat may be 'meow'. As this example shows, class instance variables are used mainly for various constants. Note that class instances cannot access them directly just as a class cannot directly access instance variables of their instances; in both cases, access requires accessor methods. Class instance variables are relatively rare.
It is sometimes useful to create objects that can be shared by several otherwise unrelated classes. As an example, many classes use various 'text constants' such as the ASCII code of the backspace character, the italic style of text, or the 'centered paragraph' style of paragraphs. None of the variables discussed so far allows this because their scope is limited. This is why VisualWorks has shared variables. These can be defined in a class (see the shared variables button in a Browser) and accessed by the class and its subclasses or their instances, or in a separate namespace. Two examples of class-based shared variables are Pi and RadiansPerDegree, both defined in classes Double and Float. An example of the second is namespace Graphics.TextConstants, which includes numerous constants divided into several categories such as Characters and Emphases. Shared variables are relatively common and they replace the concepts of pools and class variables (not to be confused with class instance variables) used in earlier versions of Smalltalk.
Exercises:
This completes the introduction to Smalltalk principles. In the next part, I will introduce the essential Smalltalk classes and explain some additional principles.
Although learning Smalltalk certainly does not require knowing all classes in the library, you cannot write Smalltalk code without knowing about the fundamental classes such as those implementing numbers and strings; this is the subject of this part of the Introduction. Additional important classes are described later in this tutorial. Once you start using Smalltalk, you will quickly learn the other classes that you need most often.
Because Object is the superclass of all classes, all classes inherit all its methods. The most common ones and their protocols are listed and explained below.
Transcript show: (5 * 3) printString
or in dialogs as in
| price |
price := 100.
Dialog warn: 'The price is ', price printString
The basic definition of printString in Object is not very useful because it essentially only returns the name of the class, and if you define a new class, you must change printString behavior by defining your own printOn: method. To do this, follow one of the many examples in the class library.
To see the difference between equality and equivalence, consider two Book objects defined to be equal when they have the same author and title. Two books with the same author and title but a different publisher will then be equal but not the identical book.
As another example, evaluateFor many objects, = and == give the same result, because Object defines = to be the same as == and many classes don't override this definition. But don't take = and == for granted because sometimes they don't give intuitively obvious results.
| p1 p2 | p1 := (Point x: 10 y: 20). p2 := (Point x: 10 y: 20). Transcript clear; show: (p1 = p2) printString. "true - two points whose x and y are equal because Point defines = that way." Transcript cr; show: (p1 == p2) printString "false - these are two equal but distinct objects."
As an example
'abc' = 'abc' "true"Be especially careful with numbers:
'abc' == 'abc' "false"
123 = 123.0 "true"and especially:
123 == 123.0 "false"
123 = (122 + 1) "true"but:
123 = (122 + 1.0) "true"
123 == (122 + 1) "true"In the above examples, the differences lies in the number-classes' definition of = as "having the same value". Therefore, the integer "1" compares equal to the rational number "1.0", as they have the same value. However, they are not the identical object: one is an Integer, the other is a real number.
123 == (122 + 1.0) "false"The situation is even more confusing as some numeric classes share instances while others do not; for example, in the addition examples above, the result of "1+1" is identical to "2", as integer numbers are identically reused (the add-operation returns an already existing instance of a number which represents the integral value "2"), whereas the rational number arithmetic creates new instanes for the result.
It is highly recomended, that you use the equal operator = and NOT the identity operator == for numerics.
Finally,
copying methods make copies of the receiver and create a new object of the same kind as the receiver. The exact relation between the receiver and the copy depends on how certain methods are defined ('copying semantics') as I will show next.
The two main methods in this protocol are copy
and postCopy
.
Method copy
first makes a 'shallow copy' of its receiver and then sends a
postCopy
message to it to perform any other desired copy operations.
The definition of postCopy in Object does not do anything, but many classes redefine postCopy to perform their customized copying operations.
Misunderstanding the copy operation is a frequent cause of strange program behaviors and I will now explain it in more detail.
Assume the following classes:
Book
- has some instance variables including borrower, an instance of Person
Person
- has some instance variables including homeAddress
Assume that Book does not redefine postCopy. If you take a Book object and make a copy, you will get a 'shallow copy', a new Book object whose instance variables are the values of the instance variables of the original. In particular, the borrower of both books will be the same Person object. If you now change the address of the borrower of the copy of the original book, the original book's borrower's address will also change, because the borrower objects of the original and the copy are one and the same object and you have just changed this object's address. The same thing, of course, happens if you change the original's address - the copy's address will also change. If you do not desire this behavior, redefine postCopy in class Book accordingly. For examples, browse some implementors of postCopy. In essence, a 'deeper' copy will require that you define postCopy to create copies of instance variables to the desired depth.
Method deepCopy
copies the receiver object and
then (recursively) walks over all referred to objects and copies them also.
Be careful, as this might create huge copies - which is usually not what is required.
(Non ST/X users: and also be careful to avoid loops and self-references in the object)
Exercises:
=
" in class Object
.
=
". (Hint: Use command "Implementors Of..." in the launchers menu or in the browser.)
Object
is "isMemberOf:
".
It takes a class as its argument and returns true if the receiver is an instance of the argument class.
Try "3/5 isMemberOf: Number
", or "3/5 isMemberOf: Fraction
" and comment on the result.
isKindOf:
" is related to "isMemberOf:
".
Read its definition in the Browser and try "3/5 isKindOf: Number
", "3/5 isKindOf: Fraction
".
Explain the commonalities and differences between the two messages.
respondsTo: aSymbol
" answers true if the receiver understands the message whose selector is aSymbol.
Try "3 respondsTo: #squared
", "3 respondsTo: #asUppercase
",
"3 respondsTo: #blaBlaBla
",
and "'abc' respondsTo: #+
". Find at least three references to this message in the library.
copy
and postCopy
in Object
.
Then read the definition of postCopy
in Rectangle
(ST/X: in Text) and explain its effect.
Object
,
evaluate "Object selectors size
" with "Print it".
Smalltalk number classes include integers, floating-point numbers, fractions, fixed-point numbers (fixed number of decimal digits), complex numbers (extension of the basic library in a parcel), metanumbers (parcel including infinity and other unusual but useful kinds of numbers), and others.
Numbers define the obvious protocols for arithmetic and mathematical functions.
Check them out, for example, in class ArithmeticValue
and try the following examples with "Print it",
carefully considering the effect of evaluation rules on the order of calculation:
3 + 7 / 3 "A message from the arithmetic protocol."
(3 + 7 / 3) asFloat "asFloat is in the converting protocol."
(3 + 7 / 3) asFixedPoint: 2 "asFixedPoint: is also in the converting protocol."
Float pi asRational "Also in the converting protocol."
15 log "log is in mathematical functions."
0.3 sin "sin is in mathematical functions."
1000 factorial "Protocol factorization and divisibility."
37 raisedTo: 22 "raisedTo: is in mathematical functions."
Some of the interesting aspects of number classes include the fact that all numbers are instances of classes (unlike most other languages where they are special data types and thus subject to different rules than objects), that they perform automatic conversions between 'large' and 'small' integers, and that you can extend their protocols - because they are objects (you did define the cubed method in an earlier exercise).
16rF8 + 2r00001000 "constant can be input in hex, binary and other bases."
Exercises:
Float
, Double
, Fraction
, and FixedPoint
?
FixedPoint
? (Hint: Read the class comment.)
FixedPoint
with two decimal digits. (Hint: See the class comment.)
Float
?
How is its implementation in Double
different?
factorial
" ? What about the other messages in the same protocol?
A string is an indexed collection of characters and as such it understands numerous messages for concatenation, substring insertion, searching, and other useful operations.
Most of the methods commonly used with strings are defined in String
and its superclass CharacterArray
,
but many are inherited from the collection superclasses of String.
Class String itself is an abstract class and factors out shared behaviors of several different implementations of strings.
(That is VisualWorks specific and might not true for other Smalltalk implementations)
As an example of string messages, evaluate each of the following lines with "Print it":
'abc' < 'xyz'
'abcdefg' findString: 'de' startingAt: 1 "Returns the index or 0 if not found"
"Elements of String and other indexed collections begin at index 1."
'abcdefg' size
'abcdefg' copyFrom:2 to:4
To see the nature of string objects,
evaluate the following with "Do it" and examine its
numbered 'indexed elements'.
'abcdefg' matchesRegex:'ab.*fg'
Exercises:
'abc' inspect "VW users: What is the class of this string?"
CharacterArray
and write an expression using each of them.
new:
message as in
"String new: 16
".
This creates a String with no characters in it but room for 16 characters. Inspect this object and note its class and explain.
Note: most Smalltalk systems support multibyte strings (TwoByteString
, UnicodeString
etc.)
to represent characters outside the 8-bit ascii character set.
Character
.
Characters are usually created as literals as in ("Inspect it"):
or by a class-side instance creation message such as
You can also create a character by conversion from its numeric code as in
Character value:8 "Character from its ASCII code."
80 asCharacter "Character from its ASCII code."
or extract them from strings, as in:
'hello' at:2 "The 2nd element in the string"
Class Character
is a subclass of the abstract class Magnitude
(as are Date
, Time
and other 'comparable' objects, in particular all number classes)
and as such its instances can be compared as in
Notice, that a characters code is not limited to the 0..255 range of ASCII;
most Smalltalk implementations support (at least) 2-byte unicode, as in:
Character value:16r10FF
Exercises:
Character
.
Magnitude
is an abstract class gathering all behaviors meaningful for comparable objects.
All its methods are derived from a few methods left as subclass responsibility.
What are these three methods and how are the other methods derived from them?
Note that due to inheritance, subclasses of Magnitude
only have to define these methods and inherit all the other functionality,
thus saving many redefinitions and enforcing consistency.
Magnitude
and use it with
strings, dates, time objects, and numbers (all instances of Magnitude
subclasses).
asString
that returns a String with the receiver character as its only element.
As an example, "$a asString
" should return 'a'. Hint: use the with:
instance creation message.
Date
, define method dayToday
such that "Date dayToday
" returns the name of the day today as a String.
As an example, "Date dayToday
" might return 'Monday'.
hourNow
such that "Time hourNow
" returns the integer hour of time now.
As an example, "Time hourNow
" might return 7 or 15 for a.m. and p.m. time respectively.
Symbol
, a subclass of String
) are pretty much
like strings, but their instances are unique whereas strings are not.
#'abc'
and
#abc
represent the same (identical) object.
asSymbol
message to a string.
To see the difference, between symbols and strings, try
'abc' = 'abc'
'abc' == 'abc' "These are two different String objects that happen to have the same value."
and
#'abc' = #'abc'
#'abc' == #'abc' "Because symbols are unique, this returns true."
Some messages require Symbols as arguments, usually when the arguments are names of methods or classes. Because of this, its system primitives protocol includes selector-related messages such as
#+ isKeyword "Tests whether + is a keyword message."
#'between:and:' isKeyword "Tests whether #'between:and:' is a keyword message."
#'between:and:' keywords "Extracts keyword components from selector between:and:"
and others. An interesting message that requires a Symbol as its argument is
perform:
and its relatives.
This message is defined in Object
and thus understood by all objects
and it tells its receiver to evaluate the Symbol argument as a message.
As an example,
tells 3 to evaluate factorial; it is thus equivalent to
A typical use of perform:
is to execute a message associated with a button in a window: The programmer can associate any message with a button (by naming it as a Symbol),
and when the button is clicked, the action message is 'performed'.
A nice demonstration of perform:
is:
Try this and enter the names of various binary selectors, such as '+', '-', '*' or even 'raisedTo:'etc.
| opNameString operation |
opNameString := Dialog request:'Perform which operation ?'.
operation := opNameString asSymbol.
10 perform: operation with: 5.
Exercises:
3 perform: #'+' with: 5
" and "3 perform: #'between:and:' with: 7 with: 15
".
Find the protocol defining perform: methods and comment on it.
at:put:
message ? Why must this be as it is ?
Dialog request: 'What is your age?' "Returns a string - no arithmetic possible!"
Dialog request: 'What is your age?' initialAnswer: '20' "Returns a string - no arithmetic possible!"
(Dialog request: 'What is your age?' initialAnswer: '20') asNumber "Returns a number - can do arithmetic."
Dialog "Returns selection given by the values: argument"
choose: 'Which one do you want?'
fromList: #('first' 'second' 'third' 'fourth') "Prompt labels."
values: #(1 2 3 4) "Corresponding return values."
lines: 8 "Number of lines displayed."
cancel: [#noChoice] "Value returned when user clicks Cancel."
Dialog confirm: 'Are you sure you want to delete the file?' "Returns true or false"
Dialog warn: 'This is a warning' "Returns nil - the UndefinedObject"
Dialog information: 'This is some information' "Returns nil - the UndefinedObject"
Exercises
A String is a relatively primitive objects - a sequence of characters with no 'rendering' information (font, color, size, etc.).
Consequently, a String prints itself in the default font and default color.
If you want to control the rendering of a piece of text,
you must convert the string to a Text object and specify the emphasis of its characters.
As an example,
ComposedTextView open: 'abc' asText allBold asValue
opens a window, with the specified text in bold.
To display the text italicized, you can use
ComposedTextView open: ('abc' asText emphasizeAllWith: #italic) asValue
You can also emphasize individual characters as in
ComposedTextView open: ('abcd' asText emphasizeFrom: 1 to: 2 with: #underline) asValue
To combine several emphases, you must use an array (to be explained later) of emphasis values as in
The variable emphasis used above is not really necessary and I used it to make the code more readable. If you want to use color, you must specify it using an Association (created with the -> message and to be introduced shortly) as in
Exercises:
Text
and emphasis in the class library.
(Hint: To find references to class Text
, select the class and execute command browse... and then Class refs in the class list.)
ColorValue
lets you create objects describing colors in various mixtures of red, green, and blue, or using other specification mechanisms.
Many common color combinations such as red, salmon, yellow, and blue are predefined via class messages. Find these class messages and count how many different colors are predefined. Browse references to ColorValue and note that most of them refer to 'resources' - definitions of user interface objects such as icons and windows.
emphasizeFrom:to:with:
from the Text protocol.)
allItalic
and allUnderlined
to emphasize all characters in the receiver Text as indicated.
Class Boolean
is abstract and classes True
and False
are its concrete subclasses,
each with a single instance: true and false.
Booleans are used mainly for control of flow of execution including conditional execution of a block of statements
and conditional repetition.
Try
(4 < 5) "true"
(15 factorial < 100000000000) "see for yourself (try this in C!)"
(4 < 5) ifTrue: [Transcript clear; show: '4 is less than 5'] "The ifTrue: message. To save typing, press <Ctrl> t"
(4 < 5) ifFalse: [Transcript clear; show: '4 is less than 5'] "The ifFalse: message. Shortcut <Ctrl> f"
(14 < 5) ifTrue: [Transcript clear; show: '14 is less than 5']
(4 < 5) ifTrue: [Transcript clear; show: '4 is less than 5'] "The ifTrue:ifFalse: message - this and the following line."
ifFalse: [Transcript clear; show: '4 is NOT less than 5']
(14 < 5) ifTrue: [Transcript clear; show: '14 is less than 5']
ifFalse: [Transcript clear; show: '14 is NOT less than 5']
The square bracket constructs containing statements constitute block closures or simply blocks. Evaluation of the statements in a block is delayed until it is explicitly requested by the definition of the message as we will see when we talk about blocks in the next section.
Notice that the ifTrue/ifFalse constructs are regular message sends.
I.e. they are expressions which yield a value (they are not statements, as in many other
programming languages).
Therefore, they return a result: the value of the evaluated branch:
or even:
(3 < 4) ifTrue:[ 'less' ] ifFalse: [ ' not less' ]
(this will sound familiar to Lisp or other functional language programmers).
Transcript
showCR:( (3 < 4)
ifTrue:[ '3 is less than 4' ]
ifFalse:[ '3 is not less than' ]
)
As in all programming languages, Booleans can be combined, as in
(3 < 4) & (5 < 6) "logical AND"
(3 < 4) | (5 < 6) "logical OR"
(3 < 4) not "logical negation"
The & and | binary messages are 'fully evaluating', which means that the argument is evaluated under all circumstances.
This is because Smalltalk uses strict evaluation
which means that a messages argument(s) are evaluated before - unless the expression is
embedded in a block, which can be passed around unevaluated and forced to be evaluated later.
The and and or operators also have 'partially evaluating' versions in which the argument is only evaluated if necessary:
(3 < 4) and: [5 < 6] "The argument block is evaluated because the receiver is true,
and the expression's value thus depends on the value of the argument."
(3 > 4) and: [5 < 6] "The argument block is not evaluated because the receiver is false,
and the expression's value is thus false -
no matter what is the value of the argument."
(3 < 4) or: [5 < 6] "Argument is not evaluated."
(3 > 4) or: [5 < 6] "Argument must be evaluated."
(3 factorial > 15) and: [3 squared > 50 or: [44 > 150 log]] "Combines several logical operations."
Note that the partially evaluating version requires a block as its argument
because only a block argument gives the option to be evaluated or not.
If the block needs to be evaluated, it is evaluated inside the definition of the and:
or or:
message.
The same thing happens in ifTrue:
and ifFalse:
messages.
Although the fully evaluating version may be easier to read, the partially evaluating versions with block arguments are generally preferred for two reasons: they may be faster because the argument does not have to be always evaluated, and they allow us to avoid evaluating an inappropriate or possibly even illegal operation as in
| x |
x := (Dialog request: 'Enter a number' initialAnswer: '10') asNumber.
x > 0 and: [x log > 5]
If the user entered a negative number,
attempting to evaluate the statement inside the block would cause an exception,
but this does not happen with and:
because
the block will not be evaluated when x > 0.
On the other hand,
| x |
x := (Dialog request: 'Enter a number' initialAnswer: '10') asNumber.
x > 0 & (x log > 5)
will cause an exception for x <= 0
because the argument of &
must be calculated before the message is sent; in other words: always.
Exercises:
ifTrue:
in classes Boolean
, True
, and False
.
ifTrue:
message return when its receiver is true? What does it return if the receiver is false?
and:
and or:
messages using ifTrue:
and ifFalse:
.
True
and False
;
can you explain the reason for this?
Blocks are instances of class BlockClosure (in VisualWorks). They are almost always instantiated as literals using the square brackets syntax as in
[30 squared]
[Transcript clear. Transcript show: 'Hello']
A block represents 'delayed execution' of a sequence of zero or more statements,
which means that the expressions inside the block are evaluated only if the program explicitly requests it.
C-programmers can think of blocks as a kind of "powerful anonymous function",
Lispers call them lambdas.
To validate this, evaluate the above statements with "Print it"
and "Inspect it". In the inspector, send the block (self there) a
value
message.
To evaluate a block, in other words to evaluate the expressions surrounded by the brackets, send it a value-message, as in
[30 squared] value
[Transcript clear. Transcript show: 'Hello'] value
[:x | 3 + x] value: 5 "Another block/value-message combination that will be explained shortly."
Evaluate each of these three expressions with "Print it" and note that a block returns the object calculated by its last statement.
Blocks are very important and one of their most common uses is in iteration (to be presented later) - repeated evaluation of a sequence of statements while a condition holds, does not hold, etc. To see iteration at work, study and then evaluate the following code fragment:
The evaluation of the block argument in this example is triggered by a value
message
inside the definition of the whileTrue:
method.
This definition is, of course, in class BlockClosure
(Block)
because the code shows that the block is the receiver.
Study and evaluate also the following code fragments using different iteration messages:
The following two forms of iteration do not use blocks as receivers - they are not defined in class BlockClosure (Block) - but I include them here because they are examples of common iterations:
Transcript clear.
3 timesRepeat: [Transcript cr; show: 'Testing!']
Transcript clear.
1 to: 3 do: [: n | Transcript show:n; tab; show:n squared; cr]
The last block contains a block argument "n".
In this case, the argument takes consecutive values of 1, 2, and 3 during the repeated evaluation of the block.
That's because the definition of to:do:
dictates it, not because of some magic.
Some messages are defined so that they require one or more arguments,
others don't have an argument - it all depends on what is needed and how the message is defined.
Blocks may also contain internal temporary variables as in
The 'lexical scope' (visibility) of block arguments and block temporary variables is limited to the block itself and identifiers n, square, and cube are thus undefined outside the block. If you have a choice, define your temporary variables inside your blocks rather than outside - this not only makes evaluation slightly faster, but also (and this is the main point!) makes your code less vulnerable to errors.
Exercises:
to:do:
steps through its block argument values in increments of 1.
Is there a method that allows you to specify the step?
(Hint: The method is defined in the same class and protocol as to:do:
)
timesRepeat:
millisecondsToRun:
which takes a block as its argument.
Read its definition, and use it to determine how long it takes to evaluate "10000 factorial
" on your computer.
ifTrue:
),
one argument (as in to:do:
above),
or more than one argument is the kind of value message used to evaluate the block.
Check the definition of ifTrue:
and to:do:
to see the difference
Classes defining various kinds of collections of objects are one Smalltalk's greatest strengths. They include collections with indexed elements (array, ordered collection, sorted collection, and others), and unordered collections whose elements are not accessed by index (such as sets, bags, and dictionaries). Most collections are dynamic in that their size can change at run-time, but a few (Array and its subclasses) have fixed size. Indexed collections are indexed starting from 1. Almost all collections are heterogenous (can accept any objects as their instances) but a few are homogeneous (accept only certain kinds of objects). The following is a brief overview of the essential collection classes and their main protocols:
or, initially empty with a predefined size:
#(1 2 3 $a $b $c 'abc' 3.14 #symbol true)
|a|
a := Array new:100.
a at:50 put:'fifty'.
a inspect.
The elements of a literal array may be any literal objects, including literal arrays:
#(
(1 'one')
(2 'two')
(3 'three')
)
All the following collections are dynamic in size - elements can be added and removed at any time and without limits, and the amount of allocated space automatically grows when the current capacity is filled.
|o|
o := OrderedCollection new.
o add:'one'.
o add:'two'.
o add:'three'.
o inspect.
[:element1 :element2 | element1 < element2]
to decide whether element1 should be located before element2 or not and elements are thus sorted in ascending order. But the program can define any sort block, even during the lifetime of the collection, thus resorting the collection automatically:
to sort a collection of strings ignoring case differences, use the following sortBlock:
#(1 2 3 4 5 6 7 8)
asSortedCollection: [:x :y| (x rem: 4) < (y rem: 4)] "What is this? Predict and try."
[:s1 :s2 | s1 asLowercase < s2 asLowercase]
#(1 1 1 2 2 2 3 3 3 2.0) asSet
#(1 1 1 2 2 2 3 3 3 2.0) asSet
vs.
#(1 1 1 2 2 2 3 3 3 2.0) asIdentitySet
->
" binary message as in
'maison' -> 'house' "Entry in an French - English dictionary. Evaluate with 'Inspect it'."
'ADD' -> 2r00001101 "Possibly a translation of an assembly language mnemonic to its binary equivalent.
Note the syntax of the binary value."
Strictly speaking, elements of Dictionary do not have to be associations but must understand the Association protocol;
in reality, they are practically always associations.
To see the internals of a Dictionary, inspect
Dictionary new
add: 'CRT' -> 'Cathode Ray Tube';
add: 'SSI' -> 'Small Scale Integration';
yourself
The following example shows that duplication is eliminated on the basis of equality of keys:
I will explain the purpose of yourself
in a moment.
Both the key and the value may be any object, including collections such as arrays or other dictionaries. Since dictionaries are collections with special elements (associations) their protocol is somewhat different from that of other collections.
and compare with the expression above.
IdentityDictionary new
add: 'key1' -> 'old value';
add: 'key1' -> 'new value';
yourself "Two entries with 'key1' "
Be aware that Set, IdentitySet, Dictionary and IdentityDictionary are highly efficient
for larger sizes: they show O(1) time behavior. That means that their access time is
constant no matter how many elements they contain. This is much better than array-, orderedCollection-
or linkedList time behavior, which is O(n) for searching (i.e. search time grows
linear with the number of elements).
However, the hashing algorithm used in sets and dictionaries creates some constant overhead, so for
very small collection (up to maybe 5 elements), the non hashing collections might still outperform the
hashed ones.
Interval from: 3 to: 9 "Represents sequence 3, 4, 5, 6, 7, 8, 9"
Interval from: 3 to: 9 by: 1 / 2 "Represents what?"
new
, new:
, and various forms of with:
as in
Array new: 5
OrderedCollection new
Array with:1 with:2 with:3
Array with: 3 factorial with: 5 factorial with: 23 factorial
All collections where the elements can be accessed via a key (either numeric or another object)
respond to the access messages "at:
" and "at:put:
"
Collections which can grow and shrink and where elements can be added/removed, respond to
the "add:
" and "remove:
" family of messages.
Arrays, OrderedCollections, Dictionaries (and others)
respond to getter-messages at:
and setter-messages at:put:
as in
OrderedCollection often uses first
and last
, and removeFirst
and removeLast
for accessing.
The second pair of messages returns the same object as the first pair, but removes the object at the same time.
Methods in adding and removing protocols (explained below) are frequently used as setters to fill an OrderedCollection.
Of course, first
, last
etc. are only supported by collections which impose an order on their
elements (i.e. Sets, Bags and Dictionaries do not).
Dictionaries are special and use their own accessors. Check them out in the Browser.
#('abc' 'AAA' 'xyz' '123' 'def') asSortedCollection "Use Inspect or Print it.
The receiver is a literal array."
#(1 1 1 3 5 6 6 2 2) asSet
(1 to:100) asOrderedCollection "Use Inspect or Print it.
The receiver is an Interval."
#('abc' 'AAA' 'xyz' '123' 'def' 'abc')
asSet asSortedCollection asArray "Eliminate duplication and sort an array."
(1 to:100) as:Array
add:
, remove:
and remove:ifAbsent:
(used if you are not sure that anObject really is in the collection).
A peculiarity of all these messages is that they return the argument, not the collection itself.
So
adds 'Wayne' to names, but the add: message returns 'Wayne', not the changed collection - so that the fragment prints only the name. To obtain the collection, cascade add: with message yourself as in
The remove:
messages also return the argument to be removed.
Compare the value of
with
This peculiarity of adding and removing messages applies to other collection accessors as well
and is a source of frequent mistakes, even by experienced Smalltalk programmers.
(It could be argued, if the value returned by the add:-messages should not be changed...)
Finally, read, predict, and test the following code fragment:
OrderedCollection can also use addLast:
(which is equivalent to add:
), and addFirst:
In Dictionaries the argument of add:
is an Association,
and remove:
and remove:ifAbsent:
are illegal
- use removeKey:
and removeKey:ifAbsent:
instead. Inspect
actually, since the associations are only present virtually (actually the dictionary stores keys and values internally
in separate arrays), you can (should ?) use the alternative accessors
| dictionary |
dictionary := Dictionary new
add: 'overdo' -> 'do to death, go to extremes';
add: 'overheated' -> 'agitated, excited';
add: 'playmate' -> 'buddy, companion';
yourself.
dictionary removeKey: 'overdo'.
dictionary
at:
and at:put:
with dictionaries:
which is actually more efficient, as the temporary associations are not created.
| dictionary |
dictionary := Dictionary new
at: 'overdo' put:'do to death, go to extremes';
at: 'overheated' put: 'agitated, excited';
at: 'playmate' put: 'buddy, companion';
yourself.
dictionary removeKey: 'overdo'.
dictionary
#(1 2 3 4) do: [:element|
Transcript show: element squared; cr
]
and the following (line-by-line) with Inspect or "Print it"
#(1 5 2 89 34 53)
select: [:element| element > 28]
#(1 5 2 89 34 53)
reject: [:element| element > 28]
#(1 5 2 89 34 53)
collect: [:element| element > 28]
#(1 5 2 89 34 53)
detect: [:element| element > 28] "Returns the first element satisfying the condition."
#(1 5 2 89 34 53)
detect: [:element| (element rem: 3) = 0]
ifNone: [Dialog warn: 'No such element']
#(1 5 2 89 34 53)
findFirst: [:element| element > 28] "Returns the index of the first satisfying the condition."
For dictionaries, both enumeration of associations and of association values is possible.
There is even a very valuable enumeration message, which passes both key and value as separate
arguments to the enumeration block:
For functional language programmers: these are pretty much similar to map and other functions which
use a function as argument (remember: blocks are the equivalent to lambda functions).
|d|
d := Dictionary new.
d at:'one' put:'eins'.
d at:'two' put:'zwei'.
d at:'three' put:'drei'.
d keysAndValuesDo:[:k :v|
Transcript show:k; show:' -----> '; show:v; cr
]
#(1 2 3 4 5) includes: 4 "Test for presence of a specific object."
#(1 2 3 4 5)
contains: [:number| number squared > 50] "Test for presence of an element satisfying a test."
Two very frequently used testing messages are isEmpty
and isNotEmpty
.
#(10 20 30 40 50) copyFrom:2 to:4
#(10 20 30 40 50) copyFrom:3
#(10 20 30 40 50) copyTo:4
#(50 20 40 30 10) asSortedCollection copyTo:4
(1 to: 100) copyFrom:50
#(10 20 30 40 50) , #(6 7 8) ", (comma) is the concatenation message"
'hello' , ' ' , 'world'
#(1 2 3 4) , 'hello'
Exercises:
Collection
and its subclasses and list and explain additional enumeration methods.
yourself
message. Find several of its uses in the library.
Dictionary
and IdentityDictionary
paying special attention to adding, removing, and enumeration - these protocols are somewhat different from those of other collections.
isEmpty
and explore a few uses.
IdentitySet
and Set
.
allButFirst
that returns the collection of all elements of the receiver except the first one.
As an example, "#(1 2 3) allButFirst
" should return #(2 3).
sameSizeAs: aCollection
to return true if the receiver and the argument
collections have the same size.
equalElementsAs: aCollection
to return true if the receiver and argument collections'
elements are equal. It should work for all sequenceable collections.
=
and equalElementsAs:
.
+
to add together elements of two sequenceable collections of equal size.
As an example, "#(1 2 3) + #(10 20 30)
" should return #(11 22 33). Ignore possible error conditions.
flattened
which returns a sequenceable collection of all eleements of the receiver or
the receivers collection elements' elements in the original order.
As an example, "#(1 (2 3 4) (5 6 7) (8 (9 10))) flattened
" should return #(1 2 3 4 5 6 7 8 9 10).
Ignore possible error conditions.
This part of the introduction covers the fundamentals of the following additional classes
ExternalStream
and Filename
- work on files and directories.
BinaryObjectStorage
and its relatives (BOSS) - save objects in files and retrieve them.
Exception
and its subclasses - deal with exceptional behavior such as division by 0
or out-of-bounds access of arrays.
Study the online documentation and browse the class library and the list of parcels to learn more.
The following presentation focuses on the use of files to store your application's objects in files and read them back. It presents basic storage-related classes in the following order:
Filename
provides the interface between Smalltalk and the file system of your operating system.
ExternalStream
classes implement mechanisms to open files, read or write them byte-by-byte,
and perform other file operations.
Filename
provides unified platform-independent access to files and directories.
Because file systems are platform-dependent,
Filename is an abstract class and the real work is implemented by its concrete subclasses.
This dependency is hidden because when you create a Filename the method creates an instance of the
default concrete class for the current platform.
The following are the essential Filename protocols and methods and a few examples of their use:
A Filename (which may represent a file or a directory) is created with message
named:aString
where aString is the name of the file as in
You can also create a Filename object from a String with asFilename as in
'C:\temp\help\notes.txt' asFilename "Example with complete file access path."
Class-side utilities protocol provides a number of useful methods such as
Filename volumes "Return the names of all reachable disk volumes (disk drives)."
Class-side constants protocol provides access to platform-dependent conventions as in
defaults provides access to the current directory, as in
Filename currentDirectory "Return the Filename representing the current directory."
and other information.
The following are Filename instance-side protocols:
contentsOfEntireFile
- return the contents of the file as a String. Does not require opening and closing the file.
dates
- return Dictionary with file access dates; contents depend on the platform.
fileSize
- answer the integer size of the receiver file in bytes
directoryContents
- return the contents of the receiver, assumed to be a directory
copyTo: aFilename
- copy receiver's contents to aFilename without affecting the receiver
delete
- delete receiver Filename (a file or a directory)
makeDirectory
- create a subdirectory of the current directory with the parameters of the underlying Filename
moveTo: aFilename
- copy receiver to destination aFilename and delete receiver
renameTo: aFilename
- rename receiver, possibly resulting in a move to another directory
makeWriteable
- make receiver a writeable file
makeUnwriteable
- make receiver a read-only file
asString
- answer the file's or directory's name
pathName
- answer the file's or directory's full pathname
exists
- does the receiver file or directory exist?
isWriteable
- is the receiver file or directory writeable?
isDirectory
- is the receiver a directory (rather than a file)?
'C:\tmp\mydirectory\test.txt' asFilename head "return the directory prefix of the receiver - here 'C:\tmp\mydirectory'"
'C:\tmp\mydirectory\test.txt' asFilename tail "return String with name of receiver without path, here 'test.txt' "
'C:\tmp\mydirectory\test.txt' asFilename extension "return String with receiver's extension, such as '.txt' "
unix:
'C:\tmp\mydirectory\test.txt' asFilename directory "return the Filename representing the receiver's directory
or the receiver itself if it denotes a directory,
similar to message head except that it returns a Filename object
rather than a String."
'/tmp/mydirectory/test.txt' asFilename head
'/tmp/mydirectory/test.txt' asFilename tail
'/tmp/mydirectory/test.txt' asFilename extension
'/tmp/mydirectory/test.txt' asFilename directory
Exercises
Althoug Filename provides an interface to the file system, it does not provide many messages to operate on the contents of the file except contentsOfEntireFile and a few others. This is the role of various external stream classes that are covered next.
External streams are subclasses of Stream and form one of the branches of its subtree. The other very important branch contains internal streams that are used mainly for efficient manipulation of strings and other sequenceable collections. Many of the messages described below are inherited from abstract superclasses common to both internal and external streams but their description is written in the context of external streams.
The branch leading to external streams forms a rather large hierarchy of mostly abstract classes and the following instance variables are essential for basic stream use:
position
readLimit
writeLimit
The following summarizes the essential behaviors required for use of streams with files:
External streams are created over Filename objects by messages in the stream creation instance protocol in class Filename. Several types of streams can be created and they differ in how they access the underlying file. The basic types are read stream, write stream, and read-write stream but other types of access can also be prescribed. The following are examples of the essential messages:
aFilename readStream
aFilename writeStream
aFilename readWriteStream
atEnd
byte-oriented reading and writing
next
- return next byte (character) in file; update position
nextPut:aByteOrCharacter
- store aByteOrCharacter and update position
nextPutAll:aCollection
- store aCollection's elements one-by-one in consecutive positions; update position
In accessing a text file, nextPut:
would be used with a Character argument
and nextPutAll:
would be used with a String argument.
In binary mode, the arguments must be small integer values or collections of small integers respectively.
close
- close underlying file
Read the data back one character at-a-time displaying the characters in the Transcript one per line, close file and delete it:
Notes:
nextPut:
and next
is usually both inefficient
and tedious for the programmer.
See the stream protocol for many useful messages; especially look at upTo:
,
nextWord
, nextLine
etc.
There is even enumeration protocol to treat the whole file as a collection of lines and enumerate a block for each line.
ensure:
message as described in the chapter on exceptions below to make sure the close is
always performed.
nextPut:
and nextPutAll:
.
show
and cr
messages. So the Transcript
and streams can be used interchangeable.
The style used above can be used to store and read characters and strings in text files, but complicated objects such as collections of Book objects that include Author objects, and so on should use BOSS as explained next.
For routine use, the only BOSS class you need to know is BinaryObjectStorage
.
It includes the streaming protocol and to read or write objects using BOSS you just use the accessing
and positioning methods described above.
The only other methods you need are onNew:aStream
(to prepare a Stream for writing)
and onOld:aStream
(to prepare to read objects from an existing file).
Note that the whole array was stored as one object rather than a sequence of elements. The following variation stores the elements separately and reads them back one-by-one:
With random access, BOSS could be used as a simple database program.
Exercises:
storeOn: aStream
and readFrom: aStream
.
Message storeOn:
creates a Smalltalk expression that
describes the object and stores it in a stream as a human-readable String.
Message readFrom:
uses the compiler to recreate the object from the expression.
This method of storing and restoring is unsuitable for more complex objects.
The following example illustrates the use of this approach with internal streams
- external streams would be used similarly.
Note that you don't need to close an internal stream.
Good programs must be prepared for illegal conditions such as attempts to divide by a variable whose value is 0,
reading from a file that does not exist, or accessing an array index outside its legal bounds.
One way to deal with such conditions is to detect them and prevent problems,
another is dealing with the problem when it occurs.
Prevention consists of conditional logic with expressions such as collection detect:ifNone:
or aFilename exists ifTrue: [...] ifFalse: [...]
,
whereas the problem handling approach uses Exception objects.
This section explains the principles of Exceptions.
A Smalltalk Exception is an object designed to help in handling error conditions,
an instance of one of the subclasses of Exception
.
According to VisualWorks documentation,
'exceptions are unusual or undesired events that can occur during the execution of a VisualWorks application',
and exception handling consists of defining how to handle a particular exception and 'raising an exception'
(instantiating one of the exception classes) when the 'unusual or undesired event' occurs.
Claus: I dont like this definition, it gives too much of an impression of an exception always
being an erroneous situation. However, once we dont do think so, there appear many other useful
applications of the exception mechanism, such as non-local gotos,
very late binding, notifications and queries etc.
So, an exception could also be defined as an irregular control flow.
Its as usual: for a hammer, everything looks like a nail!
To use the exception-handling system, one must understand
- the classes that define exception objects
- the methods that allow exception handling, and
- the principle of their operation.
The following is a brief summary of these topics; VisualWorks users should read the Application Developers Guide for more details, Smalltalk/X users may want to read the ST/X online documentation on Exception handling.
All exceptions are subclasses of class GenericException. This branch of the class tree is quite large as you can see by printing
GenericException allSubclasses size
Class GenericException
defines a number of instance and class variables.
They include messageText (the error string),
originator (the object that instantiated the exception),
notifierString (the default string used to describe the receiver),
initialContext (the initial context of the message send in which the exception was raised), and others.
The most important subclasses of GenericException
are Error
, Warning
,
and Notification
and the difference between them is in the severity of the event that raised them.
Error
is the superclass of exception classes raised when a serious program error
that requires an intervention occurs;
its subclasses include ArithmeticError
, DomainError
, ZeroDivide
,
RangeError
, MessageNotUnderstood
, KeyNotFoundError
,
SubscriptOutOfBoundsError
, and many others.
The default response to an Error is to open a notification window or debugger as happens,
for example, when you evaluate
but the program may define a handler that handles the situation programmatically and eliminates the exception window. I will show this shortly.
Warning
is a less serious exception raised when the user should be to be notified
of some exceptional event by a dialog rather an exception window.
The default action for a Notification
is to do nothing and continue execution;
the presence of this exception can, however, be handled using the same techniques as with other exceptions.
You can define your own exception classes as subclasses of Exception
and activate (raise) them at appropriate places of your program with messages raiseSignal
or raiseSignal: aString
.
More commonly, you will only need to intercept (trap) some of the predefined exceptions and handle them
in ways similar to the examples included in the class-side protocol examples of class Exception
.
Note, however, that (some of) these examples are written in terms of the older exception handling mechanism
that used Signals rather than exceptions.
In essence, Signals map to exceptions and have been introduced for conformity with the ANSI Smalltalk standard.
Every exception has its default handler action (method defaultAction
, usually inherited)
which is executed when the exception occurs and the program does not provide an explicit error handling action.
As an example,
3 / 0 "Handled with default action. For explicit handling by program, see example below."
by default opens the familiar Exception window, whereas a Warning by default opens a dialog.
If you anticipate the possibility of an exception and want to handle it,
you must 'guard' the operation that might create the exception by inserting it in a block,
and send it the message on: anExceptionClass do: handlerBlock
.
The on:
argument is the name the class of the anticipated exception,
and handlerBlock is a block that will be evaluated if the exception of the specified class
or its subclass is raised during the evaluation of the guarded block.
As an example, to guard against division by 0, you could write
When this code fragment is evaluated and divisor ~= 0, the receiver block performs the indicated division and evaluation proceeds to the Transcript statement. When divisor = 0, the handler evaluates the do: block and proceeds to the Transcript statement.
The principle of the the operation of Smalltalk error handlers is as follows:
As the receiver block of on:do:
is being evaluated it produces a sequence of nested messages sends in the usual way.
Each invoked message puts an 'evaluation context' object with information about the message,
its receiver, and its arguments on an evaluation stack.
Quite possibly, a large number of contexts are thus piled up on top of the context of the method evaluating the
on:do:
message as evaluation of the block proceeds.
If the evaluation of a message succeeds without raising an exception,
the piled up messages unwind one-by-one and eventually strip the stack down to the context object of the on:do: message.
If, however, a message executed before the guarded block is fully evaluated raises an exception,
Smalltalk starts searching the context stack from this message's context (now on the top of the stack)
until it finds an on:do: message whose on: argument is the exception class that was raised, or its subclass.
It then evaluates the do: block and execution proceeds from this point.
If a matching on:do: message is not found, the default action is invoked opening an Exception window.
IndexNotFoundError
- raised on attempt to access a non-existent collection index
KeyNotFoundError
- raised on attempt to access a non-existent dictionary key
NotFoundError
- raised on attempt to remove a non-existent collection element
PositionOutOfBoundsError
- raised on attempt to position a stream outside its limits
The do: exception provided to the handler block can be asked to respond in special ways. Two of them are illustrated below and others are described in VisualWorks or ST/X documentation and demonstrated in several examples in class Exception. All these methods are defined in class GenericException and you can, of course, define your own additional methods using information available in Exception objects if you wish.
"Press <Ctrl> y (ST/X: <Ctrl> c) repeatedly to see the effect of the following."
[1 to: 1000 do: [:i | Transcript show: ' ', i printString; cr. Delay waitForSeconds:0.01. ]]
on: UserInterrupt "Trap <Ctrl> y (ST/X: <Ctrl> c),
which normally causes user interrupt and opens an exception window."
do: [:ex |
Dialog warn: 'You pressed the Interrupt key'.
ex resume "Return to the loop inside the guarded block."
]
Another way to deal with exceptions is to use ensure: aBlock
or ifCurtailed: aBlock
messages.
As on:do:, both are defined in class BlockClosure
and start by evaluating the receiver.
The ensure: message then evaluates the block argument aBlock - whether the receiver block is evaluated successfully or not.
A typical use of ensure: is in processing files where we must ensure that the external stream is closed
whether the file operations succeeded or not.
The pattern is as follows:
[open stream on Filename and do something]
ensure: [stream close] "Close stream whether the operations in the block succeeded or not."
or
Message ifCurtailed: aBlock
evaluates aBlock only if the evaluation of the receiver fails.
As an example,
If the divisor is not 0, the argument block is not evaluated.
As you have noted, these two messages do not handle the raised exceptions, which perform their default actions (such as opening an exception window) but 'tolerate' (ensure:) or 'sense' (ifCurtailed:) their occurrence.
Exercises
Note: This chapter certainly needs more flesh.
This section explains the basics of creating a graphical user interface (GUI) for your application. Read on-line help or VisualWorks or ST/X documentation for more details.
Typical application development starts with 'domain' classes followed by the development of the GUI. As an example, in a library information system, you would first create classes representing books, patrons, catalogs, and other library objects, and when you are sure that they work you would then create the windows for interacting with them. To create the windows, you would use the UI Painter tool to 'paint' widgets on a blank window ('canvas') and write the 'glue' code to interface the GUI to your domain objects. You could also create windows programmatically, possibly even at runtime.
The UIPainter is accessible via a button on the Launcher or through the Tools menu. Its interface consists of three windows: the canvas itself, a Palette with widgets, and a Painter tool. To create a window, you can proceed as follows:
Besides storing a description of your window, the application model class (our NewApplication) has several other functions. As I already mentioned, NewApplication open will open the GUI and probably the whole application with it. In doing this, it will create an instance of UIBuilder which will build the window from the specification stored in windowSpec, sending several intermediate 'hook' message to the application model, the new instance of your NewApplication. By default, these messages do nothing but you can redefine them to intervene into the building process, for example to initialize the contents of some window widgets. The application model also provides an interface between the GUI and your domain objects so that, for example, when the user enters the name of a book, the application model propagates the value to your library classes. Finally, the builder provides a permanent interface to the window and allows you, for example, to enable or disable widgets, change their labels, etc.
There are, of course, lots of details that you must learn to build a more sophisticated GUI and you can find them in On-line help and in the documentation. The most important additional details concern the nature of the widgets. The essential facts about the basic widgets include the following:
Exercises
value:
"-message
without first converting it to a String.)
ApplicationModel
, SimpleDialog
, and UIBuilder
.
UIBuilder
to see how to build windows programmatically.
Note: the following applies to VisualWorks users; corresponding ST/X text has to be added.
A software application in conventional programming language usually consists of an executable (.exe) file and supporting files. The structure of a typical VisualWorks application usually consists of two main files:
The development environment that you are now using is also essentially an application, but it uses two additional files. One contains the source code and has extension .sou (this file is not usually supplied with a commercial application), and the other is the 'changes' files with extension .cha, which contains all the changes that you made to the image and the source code. The .cha file allows you to restore modified code up to the point where the changes file has been created or 'compressed' into the image; this topic is covered in the documentation. A commercial application would not have a changes file because it would be compressed into the image file. In addition, the image file would be 'stripped' of all classes that the application does not need, such as Browser classes, UI Painter classes, and so on.
Most of the classes in an application usually come from the built-in library (numbers, strings, collections, windows, etc.), some possibly modified by the programmer. Application-specific classes - for example Book, Patron, Librarian, and Catalog for a book library application - are either developed specifically for the application or come from another source. Some classes may be obtained from parcels separately loaded into the basic image.
As already mentioned, the classes that constitute the application can usually be conceptually divided into three basic groups:
Creating a new application thus requires defining new methods and classes in the domain category, and application model classes defining windows and their interfaces to domain classes. Changes to built-in classes are much less common but not unusual.
The subject of methodologies used to discover classes and methods needed to implement a specification is beyond the scope of this introduction but many Smalltalk programmers like to use Extreme Programming (XP) or its variation. The process is described in several books referenced at the end and it relies heavily on frequent testing based on a testing framework that automates the testing process. The SUnit group of classes implements the framework and provides a simpleGUI to run the tests. Another essential part of XP is frequent refactoring of code (moving methods between classes, creating abstract classes when appropriate, renaming, etc.). The Refactoring Browser is a powerful tool that makes refactoring much easier and safer. You can open it from the Launcher.
Delivering (deploying) an application means delivering an image file containing the classes needed to run the application and the virtual machine executable supplied with VisualWorks. (Re-read your license to see what you can legally deliver.) Although your application has been created using the development image, this image contains a lot of code that the application does not need, such as the Browser, the Inspector, and many other classes necessary only for development. To decrease the size of the image file, 'strip-off' as many unnecessary classes as possible using a 'stripper' ('packager') program, such as the RuntimePackager. The procedure is explained in VisualWorks documentation.
Exercises
When you start a virgin VisualWorks Smalltalk, the code library contains only the most important code. Many other interesting and useful classes are stored in pairs of .pcl and .pst files containing parcels of code. They include On-line Help (parcel VWHelp - loaded automatically when you request help), advanced tools for code development, BOSS, special kinds of numbers, a code management tool for programming teams (the StORE parcel), and many others. You can also create your own parcels to hold your own code separate from the rest, save them in a file and thus reduce the size of your image, and share them with other Smalltalk users. You can also send your parcels to Cincom and they may be added to a future release. To load, save, browse, or create parcels, open the Parcel Browser from the Launcher using either the Browse -> All Parcels command or the button with a picture of a parcel on it.
Like all other components of VisualWorks, parcels are programmable objects and advanced programmers commonly access them from their programs. See the documentation for more details.
Just as programmers using other languages, Smalltalk programmers have their opinions on what is good programming style. Perhaps the essential guiding principle is that they consider readability to be of paramount importance - more than programmers in most other languages. They think that code should be succinct, self-documenting, and no other documentation should normally be required to help the experienced reader understand the code. This leads to numerous guidelines summarized in the widely recognized books by Skublics and Beck. Within this limited space, I will list only a few selected Smalltalk style rules:
Now that you learned the basics, explore Smalltalk in more depth: