Previous Page Next Page

Chapter 4
CONTROL STRUCTURES

In the previous tutorial, you learned some of Smalltalk's basic expressions. But like any language, Smalltalk cannot do much unless it can make decisions: evaluate a condition and perform an action based on the result, or repeat actions a specified or unspecified number of times. This chapter introduces you to Smalltalk's conditional expressions and control structures which perform these tasks.

As always, you can access the examples for this tutorial if you do not want to type them. Simply use the Open item in the File menu to retrieve the contents of the file chapter.4.

Comparing Objects

Smalltalk compares objects by sending messages. The normal comparisons of <, <=, >, >= and ~= are implemented as binary messages. For example, evaluate each of these expressions:

3 < 4
#(1 2 3 4) = #(l 2 3 4)

All objects understand equality, =. Many objects also define the relational operators, or ordering messages, as in this example:

'hello' <= 'goodbye'

Since comparison messages are binary messages, parentheses are often needed if there are other binary messages in the expression. For example, evaluate the following expression with and without parentheses:

5 = (2 + 3)
5 = 2 + 3

As you see from evaluating these examples, comparisons return either true or false.

Testing Objects

Many objects understand messages that let you test something about their state or condition. For example, evaluate each of these expressions:

$a isUpperCase
('hello' at: 1) isVowel
7 odd

These messages return true or false as well.

Conditional Execution

Most languages use if statements to conditionally execute a series of statements. Smalltalk uses blocks of code and messages to do the same thing. For example, look at this expression, which computes the greater of two numbers:

| max a b |
a := 5 squared.
b := 4 factorial.
a < b
ifTrue: [max := b]
ifFalse: [max := a].
^max

The comparison message a < b returns either true or false, which then becomes the receiver of the ifTrue:IfFalse: message. It, in turn, executes the corresponding block of code.

As you have seen, all messages return a result. The ifTrue:ifFalse: message, like any message, also returns a result. Evaluate the following expression:

3 < 4
ifTrue: ['the true block']
ifFalse: ['the false block']

The message ifTrue:ifFalse: returns the result of the last expression in the block that it executes. Let's look at another example:

| string index c |
string := 'Now is the time'.
index := 1.
string size timesRepeat: [
c := string at: index.
string
at: index
put:
(c isVowel
   ifTrue: [c asUpperCase]
   ifFalse: [c asLowerCase]).
index := index + 1].
^string

The above example converts all consonants to lower case and all vowels to upper case, using the c isVowel test on each letter to find out which to do.

The other messages that execute a block of code conditionally are ifTrue:, ifFalse:, ifTrue:ifFalse: and ifFalse:ifTrue:.

Boolean Expressions

The examples so far have depended on single comparison or testing messages, such as < or isVowel. But often you need to perform a compound test. To do so, you use the and: and or: messages. For example, look at the following code fragment, which tests whether a character is not a digit:

( c < $0 or: [ c > $9 ] )

The receiver of the or: message is the result of the first comparison, either true or false. The argument is a block of code whose last expression also returns true or false. The and: message works in the same way:

( c >= $0 and: [ c <= $9 ] )

To see how this is used in a complete example, evaluate this expression:

"compute the value of the first integer in a string"
| string index answer c |
string := '1234 is the number'.
answer := 0.
index := 1.
string size timesRepeat: [
c :=  string at: index.
(c < $0 or: [c > $9] )
ifTrue: [^answer].
answer := answer * 10
+ c asciiValue - $0 asciiValue.
index = index + 1].
^answer

Notice the return expression ifTrue: [^answer] in the middle of the above example. This exits the expression as soon as the first non-digit is encountered.

You can perform more complex tests by nesting the expressions. For examples, look at the following fragment, which tests if a character is a digit or one of the letters from A-F.

( c isDigit or: [ c >= $A and: [ c <= $F ] ] )

Looping Messages

You've already seen one simple looping message, timesRepeat:. Here's a simple expression that uses another simple looping message to copy a file:

"copy a disk file"
| input output |
input := File pathName: 'tutorial\chapter.2'.
output :=  File pathName: 'tutorial\copychap.2'.
[input atEnd]
whileFalse: [output nextPut: input next].
input close.
output close

You may have noticed the message atEnd in the above example. This message returns true when there are no more characters to read from the input file stream; otherwise, it returns false. The input file stream is read with the next message, which returns the next character in the file. The output file stream is written with the nextPut: message, which writes its argument to the output.

The message whileFalse is sent to a block of code with another block of code as an argument. The message repeatedly evaluates its argument block for as long as the receiver block evaluates false. When the receiver block evaluates to true, the expression closes the input and output files.

As you might expect, Smalltalk also provides a corresponding whileTrue: message. To see it, look at this graphical example, which uses Turtle to draw some polygons:

"draw several polygons"
| sides |
Window turtleWindow: 'Turtle Graphics'.
sides := 3.
[sides <= 6]
whileTrue: [
sides timesRepeat: [
Turtle
   go: 60;
   turn: 360 // sides].
   sides := sides + 1]

Simple Iterators

The Turtle example above increases the temporary variable sides from 3 to 6 by 1, and evaluates some code for each value along the way. Like most other languages, Smalltalk provides iteration statements to do this more easily. For example, here's the same expression, using one such iteration statement:

"draw several polygons"
Window turtleWindow: 'Turtle Graphics'.
3 to: 6 do: [ :sides |
sides timesRepeat: [
Turtle
go: 60;
      turn: 360 // sides] ]

The iteration message is to:do:, which has two arguments. It takes the receiver object, 3, as the lower limit of the iteration, and uses the first argument, 6, as the upper limit. The second argument is a block of code, which itself uses a block argument, sides, to draw a polygon with sides number of sides. The iteration message assigns the values 3 thru 6 successively to the block argument, evaluating the block for each value.

The to:do: message uses an increment of one. But you can also specify your own increment by using the to:by:do: message, as in this example:

"compute the sum of 1/2, 5/8, 3/4, 7/8, 1"
| sum |
sum:=  0.
1/2 to: 1 by: 1/8 do: [ :i |
sum :=  sum + i ].
^sum

The first argument, 1, is the upper limit, while the second argument, 1/8, is the increment.

Block Arguments

As you can see from the last example, a block argument is declared in the first part of the block, preceded by a colon, :, and separated from the statements in the block by a vertical bar, |. For example, here's a block with one argument:

[ :character | character isVowel ]

In this example, the block argument is character. Block arguments are a kind of temporary variable, but do not have to be declared at the beginning of the expression series.

Generalized Iterators

Blocks with arguments allow Smalltalk to supply several generalized iteration messages: do:, select:, reject:, and collect:.

The do: Iterator

The simplest of these is the do: message:

"count vowels in a string"
| vowels |
vowels :=  0.
'Now is the time'do: [ :char |
char isVowel
ifTrue: [vowels :=  vowels + 1] ].
^vowels

The do: iterator causes the string to iterate across itself and pass each character to the block. The above example is equivalent to the following:

"count vowels in a string"
| vowels string index |
vowels :=  0.
index := 1.
string :=  'Now is the time'.
[index <= string size]
whileTrue: [
(string at: index) isVowel
ifTrue: [vowels :=  vowels + 1].
index :=  index + 1].
^vowels

The do: message can also iterate arrays, as in this example:

"draw several polygons"
Window turtleWindow: 'Turtle Graphics'.
#( 3 4 12 24 ) do: [ :sides |
sides timesRepeat: [
Turtle
go: 20;
         turn: 360 // sides] ].

and the do: message can iterate file streams, as in this example:

"Strip all line feed characters (ascii 10)
from a disk file.  Answer the number of characters 
stripped."
| input output stripped |
stripped :=  0.
input :=  File pathName: 'tutorial\striplf.pc'.
output :=  File pathName: 'tutorial\striplf.mac'.  
input do:  [ :char |
char = 10 asCharacter
ifTrue: [stripped := stripped + 1]
ifFalse: [output nextPut: char] ].
input close.
output close.
^stripped

The above expression reformats an DOS ASCII text file for use on a Macintosh, replacing your computer's carriage return/line feed pair at the end of lines with the Mac's single carriage return. You might perform this type of text file conversion routinely if you are exchanging text files on a mixed DOS and Macintosh network.

The select: Iterator

A more powerful iterator is the select: message:

"count the vowels in a string"
('Now is the time' select: [ :c | c isVowel ] )
   size

The select:. message iterates across its receiver and returns all of the elements for which the argument block evaluates to true. In this case, the result is a string of all of the vowels in the original string. The message size then tells us how many elements were selected.

The reject: Iterator

The reject: message is another generalized iterator:

"answer all digits whose factorial is
less than the digit raised to the 4th power"
#( 1 2 3 4 5 6 7 8 9 ) reject: [ :i |
   i factorial >= ( i * i * i * i) ]

The reject:. message works just as select:, but answers all elements of the receiver for which the block of code returns false, instead of true.

The collect: Iterator

The collect: message evaluates the block of code for each element of the receiver and answers the collection of all of the results returned by the block:

"square each element in the array"
#(1 13 7 10) collect: [:i| i * i]

To help see the differences between select:, reject:, and collect:, evaluate each of the following expressions:

#(l 2 3 4 5 6 7) select: [ :c | c odd ]
#(l 2 3 4 5 6 7) reject: [ :c | c odd ]
#(l 2 3 4 5 6 7) collect: [ :c | c odd ]

Concluding Example

Our final example in this chapter is inspired by the limitations of DOS file names. DOS limits file names to eight characters with a three character extension. Smalltalk/V routinely compresses the long names of classes to eight character file names when you file out a class. A good algorithm for this kind of name compression is to remove any spaces and lower case vowels from the original name from right to left. If this doesn't shorten it enough, you might truncate what's left to eight characters. With names already shorter than eight characters, you might want to pad the name with blanks, and not throw out any characters. Here, then, is a Smalltalk solution (we'll forget about some of the complexities due to reserved characters and ". " extensions for simplicity at the moment):

"abbreviate a long file name to 8 characters" 
| name length |
name := 'Some Big File Name'.
length := name size.
^( name reversed reject: [ :c |
c isSeparator or: [
c isVowel and: [
c isLowerCase and: [
( length := length - 1 ) >= 8 ] ] ] ] )
reversed,'
         copyFrom: 1 to: 8

This example will return SmBgFlNm, a cryptic but DOS acceptable file name. Let's examine this example in detail. The caret (^) on the fourth line tells us that the remainder of the program will return a single result. We reverse the name so that we can throw out characters from the end of the original name first. Similarly, we reverse the result of the reject: message to put the abbreviated name back in the proper order. We then append blanks to the resulting string and return the first eight characters as the answer. Look more closely at the expression inside of the argument block to the reject: message:

[ :c |
c isSeparator or: [
c isVowel and: [
c isLowerCase and: [
   ( length := length - 1 ) >= 8 ] ] ] ] )

Remember that the reject: message eliminates only those characters for which this block evaluates to true. It's easy to see why the first three tests are isSeparator, isVowel and isLowerCase, since they are the possible characters to eliminate. The final test is more complex:

( length := length - 1 ) >= 8

This expression must evaluate to true to delete the character, and false to keep it. The expression decrements the temporary variable length. If the length is less than 8, the character is to be kept; otherwise it is eliminated. Since we initially set length to the size of the string name, this expression returns true at most name size - 8 times, which is the number of characters we want to eliminate.

What You've Now Learned

After finishing this chapter, you should be familiar with:

If you want to review any of these topics, you can either repeat the corresponding section of the tutorial, or refer to a detailed explanation in Part 3 of this manual.

Previous Page Next Page