Control Flow Loops Enumeration/en

Aus expecco Wiki (Version 26.x)
Zur Navigation springen Zur Suche springen

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

Back to Useful API Functions

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: ZeroDivide do:[:ex | -1 ]
   => -1
[ 10 / 0 ] on: ArithmeticError do:[:ex | ex class ]
   => ZeroDivide (a handler for a superclass also handles its subclasses)
[ 10 / 0 ] on: ZeroDivide, MessageNotUnderstood do:[: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:
ZeroDivide handle:[: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:
Query isHandled
   => false

Query answer:1 do:[ Query isHandled ]
   => 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: ZeroDivide do:[:ex | ex return: 0 ]
   => 0
[ 10 / 0 ] on: ZeroDivide do:[:ex | ex retryUsing:[ 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 Error with aString as messageText.
Example:
[ 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)
[ (Notification signal:'hello') + 1 ] on: Notification do:[:ex | ex resume: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
Query answer:'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
nil ifNil:[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' ]
1 to:5 do:[:i | Transcript showCR: i squared ]
   prints 1 4 9 16 25
10 to:1 by:-3 do:[:i | Transcript showCR: i ]
   prints 10 7 4 1
0 to:1 by:0.25 do:[: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:0 into:[: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:10 ifFalse: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.:

nil ifNil: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:block1 ifFalse: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:0 into:#+
   => 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.



Copyright © 2014-2024 eXept Software AG