Control Flow Loops Enumeration/en
This document gives a short overview of exception handling, control flow, loops and collection enumeration in Smalltalk code (e.g. in elementary actions). In Smalltalk, all of these are not special syntax, but ordinary messages sent to blocks, booleans, numbers or collections. Most of them are inlined by the compiler, so they are as fast as built-in statements in other languages. There are many more to be found in either the class references or via the builtin class browser.
Reference: Block, Boolean, GenericException, Error, Notification, Query, Number, Collection, SequenceableCollection
Exception Handling
Exceptions are instances of a subclass of GenericException; the most important subclasses are Error (and its many subclasses such as ZeroDivide, MessageNotUnderstood etc.), Notification, Query and Warning.
Open a class browser or evaluate "GenericException allSubclasses inspect" to see them all.
If an error is not handled, a debugger is entered (in expecco: the action fails with an error).
Handling Exceptions
- aBlock on: anExceptionClass do: handlerBlock
- Evaluates aBlock. If the exception (or one of its subclasses) is raised during the evaluation, handlerBlock is evaluated and its value is returned. Otherwise, the value of aBlock is returned.
- The handler block may have zero or one argument (the exception object).
- Example:
[ 10 / 0 ]on:ZeroDividedo:[:ex | -1 ] => -1
[ 10 / 0 ]on:ArithmeticErrordo:[:ex | ex class ] => ZeroDivide (a handler for a superclass also handles its subclasses)
[ 10 / 0 ]on:ZeroDivide, MessageNotUnderstooddo:[:ex | ex description ] => 'Division by zero...' (use "," to handle multiple exceptions)
- anExceptionClass handle: handlerBlock do: aBlock
- the same as above, with the arguments in a different order (Smalltalk/X and old VisualWorks style).
- Example:
ZeroDividehandle:[:ex | ex return:0 ]do:[ 10 / 0 ] => 0
- anExceptionClass catch: aBlock
- evaluates aBlock; returns true if the exception was raised (and aborts the block), false otherwise.
- anExceptionClass ignoreIn: aBlock
- evaluates aBlock and ignores the exception, i.e. simply continues with the default resume value. Only useful for proceedable exceptions.
- anExceptionClass isHandled
- returns true if there is already a handler for the exception (i.e. an on:do: or handle:do: for it or one of its superclasses in the calling chain). Can be used to check before raising, e.g. to decide whether to raise a notification or to do something else instead.
- Example:
QueryisHandled=> false Query answer:1 do:[ QueryisHandled] => true
What the Handler can do
Inside the handler, the exception object can be asked for details and told how to continue:
- ex messageText / description
- the message text as given when raised, resp. a description string (which uses the exception class' default text, if no messageText was given).
- ex return: value
- leave the on:do: and return value from it (the same as returning the value from the handler block).
- ex retry
- re-evaluate the protected block (usually after some repair work was done in the handler).
- ex retryUsing: anotherBlock
- evaluate anotherBlock in place of the protected block.
- ex resume: value
- continue after the place where the exception was raised; the raise returns value. Only possible for proceedable exceptions (notifications, queries, warnings), not for errors.
- ex reject
- let an outer handler deal with it (if there is none, the default action is performed - for errors, the debugger is entered).
- Example:
[ 10 / 0 ]on:ZeroDividedo:[:ex | exreturn:0 ] => 0
[ 10 / 0 ]on:ZeroDividedo:[:ex | exretryUsing:[ 99 ] ] => 99
[
[ 10 / 0 ] on: ZeroDivide do:[:ex | ex reject ]
] on: Error do:[:ex | 'outer: ', ex class name ]
=> 'outer: ZeroDivide'
Cleanup Actions
- aBlock ensure: cleanupBlock
- Evaluates aBlock and returns its value. cleanupBlock is always evaluated afterwards, both after normal termination and when the block is left due to an exception or a return. Typically used to close files, sockets or other resources.
- aBlock ifCurtailed: cleanupBlock
- Evaluates aBlock and returns its value. cleanupBlock is only evaluated if the block does not finish normally (i.e. if it is left due to an exception or a non-local return).
- aBlock on: anException do: handlerBlock ensure: cleanupBlock
- combines both.
- Example:
stream := 'data.txt' asFilename readStream.
[
... read from stream ...
] ensure:[ stream close ]
log := OrderedCollection new.
[
[ log add:1. 10 / 0. log add:2 ] ensure:[ log add:#cleanup ]
] on: ZeroDivide do:[:ex | log add:#handled ].
log
=> OrderedCollection(1 handled cleanup)
Notice that the handler is executed before the ensure block: the handler runs "on top of" the place where the exception was raised, and the stack is unwound (executing ensure blocks) only when the handler returns.
Raising Exceptions
- anExceptionClass raise
- anExceptionClass signal
- raise the exception with its default message text.
- anExceptionClass raiseErrorString: aString
- anExceptionClass signal: aString
- raise the exception with aString as messageText. signal and signal: are the ANSI names; raise... the Smalltalk/X names.
- anyObject error: aString
- raises an
Errorwith aString as messageText. - Example:
- raises an
[ Error raiseErrorString:'something went wrong' ] on: Error do:[:ex | ex messageText ]
=> 'something went wrong'
[ Error signal:'bad' ] on: Error do:[:ex | ex messageText ]
=> 'bad'
[ self error:'oops' ] on: Error do:[:ex | ex messageText ]
=> 'oops'
Instead of raising Error itself, it is better to define your own subclass of Error (or of a more specific error class), so that handlers can distinguish your exceptions from others.
Notifications
Notifications are proceedable exceptions which are used to inform an outer caller about something. If there is no handler, nothing happens and the raise simply returns nil. If there is a handler, it may provide a value with resume: and execution continues after the raise.
Notifications are typically used to report messages from functions which are used with or without a user being involved (eg. Parser messages which shall not show a dialog when running without a user interface).
Notification signal:'hello'
=> nil (no handler)
[ (Notificationsignal:'hello') + 1 ] on: Notification do:[:ex | exresume:42 ] => 43
Warning is similar, but if unhandled, a warning dialog is shown to the user.
Queries
Queries are notifications which are used to ask an outer caller for a value (similar to dynamically scoped variables). If unhandled, the query returns a default value (nil).
- aQueryClass query
- raise the query; returns the handler's answer or the default value.
- aQueryClass answer: value do: aBlock
- evaluates aBlock; any query raised during the evaluation is answered with value.
- Example:
Query query
=> nil
Queryanswer:'abc'do:[ Query query ] => 'abc'
[ Query query ] on: Query do:[:q | q resume:'xyz' ]
=> 'xyz'
Usually you will define your own subclass of Query (possibly redefining its defaultResumeValue method).
Control Flow
Conditionals
- aBoolean ifTrue: block1 ifFalse: block2
- aBoolean ifFalse: block2 ifTrue: block1
- evaluates one of the blocks and returns its value.
- aBoolean ifTrue: block
- aBoolean ifFalse: block
- evaluates the block if the condition is true (resp. false) and returns its value; returns nil otherwise.
- Example:
(3 > 2)ifTrue:['yes']ifFalse:['no'] => 'yes'
(3 > 4) ifTrue:['yes']
=> nil
- aBoolean and: block
- aBoolean or: block
- logical "and" / "or" with short-circuit evaluation: the block is only evaluated if needed.
- aBoolean & aBoolean
- aBoolean | aBoolean
- logical "and" / "or" without short-circuit (both operands are always evaluated).
- aBoolean not
- negation.
- Example:
(3 > 2) and:[ 4 > 5 ]
=> false
(3 > 2) or:[ 1 / 0 ]
=> true (the block is not evaluated)
Nil Checks
- anObject isNil / notNil
- true if the object is (not) nil.
- anObject ifNil: block
- returns the value of block if the receiver is nil; otherwise the receiver itself. Semantically this is the same as "anObject isNil ifTrue:[...]".
- anObject ifNotNil: block
- evaluates block if the receiver is not nil, passing the receiver as argument (if the block has one); returns nil otherwise.
- anObject ifNil: block1 ifNotNil: block2
- one of the two.
- anObject ? defaultValue
- returns defaultValue if the receiver is nil; otherwise the receiver. Useful to provide default values.
- Example:
nil ifNil:['default']
=> 'default'
'abc' ifNotNil:[:v | v size]
=> 3
nilifNil:[0]ifNotNil:[:v | v size] => 0
nil ? 'default'
=> 'default'
Loops
Counting Loops
- anInteger timesRepeat: block
- evaluates the block anInteger times.
- start to: stop do: block
- evaluates the block for each number from start up to (and including) stop, passing the number as argument. Does nothing if stop is less than start.
- start to: stop by: step do: block
- the same, with a step increment (which may be negative or non-integer).
- start downTo: stop do: block
- counts down by 1.
- Example:
3 timesRepeat:[ Transcript showCR:'hello' ]
1to:5do:[:i | Transcript showCR: i squared ] prints 1 4 9 16 25
10to:1by:-3do:[:i | Transcript showCR: i ] prints 10 7 4 1
0to:1by:0.25do:[:x | Transcript showCR: x ] prints 0 0.25 0.5 0.75 1.0
Conditional Loops
- conditionBlock whileTrue: block
- conditionBlock whileFalse: block
- evaluates block as long as conditionBlock evaluates to true (resp. false). The condition is checked before each iteration, so the body may not be evaluated at all.
- block whileTrue
- block whileFalse
- evaluates the block while it returns true (resp. false). The loop's body and condition are in the same block (the last expression is the condition).
- block doWhile: conditionBlock
- block doUntil: conditionBlock
- evaluates block while (resp. until) the condition is true. The condition is checked after each iteration, so the body is evaluated at least once.
- Example:
i := 1.
[ i < 100 ] whileTrue:[ i := i * 2 ].
i
=> 128
i := 1.
[ i := i * 2. i < 100 ] whileTrue.
i
=> 128
i := 1000.
[ i := i * 2 ] doWhile:[ i < 100 ].
i
=> 2000 (evaluated once)
Endless Loops
- block repeat
- block loop
- evaluates the block forever. The block should contain a return statement (or raise an exception) somewhere to leave the loop.
Leaving a Loop Early
A return statement ("^ value") inside a block returns from the enclosing method (not only from the block), and can thus be used to leave any loop.
To leave only the loop, wrap it in a valueWithExit block, which gets an exit-block as argument:
[:exit |
1 to:10 do:[:i | i > 3 ifTrue:[ exit value:i ] ]
] valueWithExit
=> 4
Enumerating Collections
- aCollection do: block
- evaluates the block for each element, passing the element as argument. For dictionaries, the values are passed.
- aCollection reverseDo: block
- the same, in reverse order (sequenceable collections only).
- aCollection do: block separatedBy: betweenBlock
- additionally evaluates betweenBlock between elements (i.e. not after the last one).
- aCollection doWithIndex: block
- aCollection withIndexDo: block
- evaluates the 2-arg block for each element, passing the element and its index as arguments.
- aCollection keysAndValuesDo: block
- evaluates the 2-arg block for each element, passing the index (key) and the element as arguments (notice the reversed argument order). For dictionaries, the key and value are passed.
- aCollection with: otherCollection do: block
- enumerates two collections of equal size in parallel; the 2-arg block gets one element from each.
- aCollection doWithExit: block
- the 2-arg block gets the element and an exit-block, which can be used to leave the loop early (as with valueWithExit above).
- Example:
#(10 20 30) do:[:el | Transcript showCR: el ]
#(10 20 30) doWithIndex:[:el :idx | Transcript showCR: idx printString, ': ', el printString ]
#(10 20 30) keysAndValuesDo:[:idx :el | Transcript showCR: idx printString, ': ', el printString ]
#('a' 'b' 'c') do:[:el | Transcript show: el ] separatedBy:[ Transcript show: ', ' ]
prints a, b, c
#(1 2 3)with:#(10 20 30)do:[:a :b | Transcript showCR: a + b ] prints 11 22 33
dict := Dictionary new.
dict at:#a put:1; at:#b put:2.
dict keysAndValuesDo:[:key :value | Transcript showCR: key, '->', value printString ]
Notice: you should not add or remove elements to/from a collection while enumerating it. Enumerate over a copy instead.
Collecting, Selecting and Searching
Instead of writing explicit loops, it is often shorter to use one of the following:
- aCollection collect: block
- returns a new collection with the results of the block.
- aCollection select: block / reject: block
- returns a new collection with the elements for which the block returns true (resp. false).
- aCollection detect: block ifNone: exceptionBlock
- returns the first element for which the block returns true, or the value of exceptionBlock if there is none.
- aCollection inject: initialValue into: block
- accumulates a value: the 2-arg block gets the accumulated value and each element.
- aCollection count: block
- the number of elements for which the block returns true.
- aCollection anySatisfy: block / allSatisfy: block
- true if the block returns true for any (resp. all) elements.
- Example:
#(1 2 3 4) collect:[:x | x * x]
=> #(1 4 9 16)
#(1 2 3 4) select:[:x | x even]
=> #(2 4)
#(1 2 3 4)detect:[:x | x > 10]ifNone:['none'] => 'none'
#(1 2 3 4)inject:0into:[:sum :x | sum + x] => 10
#(1 2 3 4) count:[:x | x odd]
=> 2
See Collection API Functions for more.
Non-blocks as Argument to Control Flow Functions
The control flow and enumeration functions above send value (resp. value:) to their block arguments. However, value is also implemented in Object, where it simply returns the receiver itself. Thus, any object can be passed where a block is expected, and constructs like:
x := (a > b)ifTrue:10ifFalse:20
will assign either 10 or 20 to x, because value is sent to either argument, and the numbers return themself.
The same is true for other control functions, e.g.:
nilifNil:0 => 0 #(1 2)detect:[:x | x > 5]ifNone:'none' => 'none'
For constants, there is no performance penalty. However, the compiler will only inline the code if the arguments are blocks or constants. Thus, with variables or other expressions as argument, a real message send is performed which is slower.
Care must be taken in the following situations:
1) If an argument is a block object (e.g. held in a variable), it will be evaluated - not returned. Thus, in:
block1 := [ 'first' ]. block2 := [ 'second' ]. (a > b)ifTrue:block1ifFalse:block2 => 'first' or 'second' (one of the blocks is evaluated)
either block is evaluated, and its value is returned. To get the block itself, write:
(a > b) ifTrue:[ block1 ] ifFalse:[ block2 ]
The same is true for other objects which redefine value, e.g. a ValueHolder returns the value it holds instead of itself.
2) Collection enumerators send value: to their block argument, and symbols respond to value: by sending themself as message to the argument. Thus, a symbol can be passed instead of a one-argument block:
#(2 4 6 1 3 5)detect:#odd => 1 (the first element for which odd returns true; the same as "#(2 4 6 1 3 5) detect:[:el | el odd]") #(1 2 3 4)select:#even => #(2 4) #('a' 'bb' 'ccc')collect:#size => #(1 2 3)
Similarly, symbols respond to value:value:, so they can also be used where a two-argument block is expected:
#(1 2 3 4)inject:0into:#+ => 10
This is convenient, but be aware that a symbol given as argument is not a constant: e.g. "coll collect:#foo" will send #foo to every element - not return the symbol #foo for each element.