11.5: Collection Iterators
- Page ID
- 39637
In Pharo loops and conditionals are simply messages sent to collections or other objects such as integers or blocks (see also Chapter : Syntax in a Nutshell). In addition to low-level messages such as to:do:
which evaluates a block with an argument ranging from an initial to a final number, the collection hierarchy offers various high-level iterators. Using such iterators will make your code more robust and compact.
Iterating (do:)
The method do:
is the basic collection iterator. It applies its argument (a block taking a single argument) to each element of the receiver. The following example prints all the strings contained in the receiver to the transcript.
#('bob' 'joe' 'toto') do: [:each | Transcript show: each; cr].
Variants. There are a lot of variants of do:
, such as do:without:
, doWithIndex:
and reverseDo:
.
For the indexed collections (Array
, OrderedCollection
, SortedCollection
) the message doWithIndex:
also gives access to the current index. This message is related to to:do:
which is defined in class Number
.
#('bob' 'joe' 'toto') doWithIndex: [ :each :i | (each = 'joe') ifTrue: [ ^ i ] ] >>> 2
For ordered collections, the message reverseDo:
walks the collection in the reverse order.
The following code shows an interesting message: do:separatedBy:
which executes the second block only in between two elements.
| res | res := ''. #('bob' 'joe' 'toto') do: [ :e | res := res, e ] separatedBy: [ res := res, '.' ]. res >>> 'bob.joe.toto'
Note that this code is not especially efficient since it creates intermediate strings and it would be better to use a write stream to buffer the result (see Chapter : Streams):
String streamContents: [ :stream | #('bob' 'joe' 'toto') asStringOn: stream delimiter: '.' ] >>> 'bob.joe.toto'
Dictionaries
When the message do:
is sent to a dictionary, the elements taken into account are the values, not the associations. The proper messages to use are keysDo:
, valuesDo:
, and associationsDo:
, which iterate respectively on keys, values or associations.
colors := Dictionary newFrom: { #yellow -> Color yellow. #blue -> Color blue. #red -> Color red }. colors keysDo: [ :key | Transcript show: key; cr ]. colors valuesDo: [ :value | Transcript show: value; cr ]. colors associationsDo: [:value | Transcript show: value; cr].
Collecting results (collect:)
If you want to apply a function to the elements of a collection and get a new collection with the results, rather than using do:
, you are probably better off using collect:
, or one of the other iterator methods. Most of these can be found in the enumerating
protocol of Collection
and its subclasses.
Imagine that we want a collection containing the doubles of the elements in another collection. Using the method do:
we must write the following:
| double | double := OrderedCollection new. #(1 2 3 4 5 6) do: [ :e | double add: 2 * e ]. double >>> an OrderedCollection(2 4 6 8 10 12)
The message collect:
executes its argument block for each element and returns a new collection containing the results. Using collect:
instead, the code is much simpler:
#(1 2 3 4 5 6) collect: [ :e | 2 * e ] >>> #(2 4 6 8 10 12)
The advantages of collect:
over do:
are even more important in the following example, where we take a collection of integers and generate as a result a collection of absolute values of these integers:
aCol := #( 2 -3 4 -35 4 -11). result := aCol species new: aCol size. 1 to: aCol size do: [ :each | result at: each put: (aCol at: each) abs ]. result >>> #(2 3 4 35 4 11)
Contrast the above with the much simpler following expression:
#( 2 -3 4 -35 4 -11) collect: [ :each | each abs ] >>> #(2 3 4 35 4 11)
A further advantage of the second solution is that it will also work for sets and bags. Generally you should avoid using do:
, unless you want to send messages to each of the elements of a collection.
Note that sending the message collect:
returns the same kind of collection as the receiver. For this reason the following code fails. (A String
cannot hold integer values.)
'abc' collect: [ :ea | ea asciiValue ] >>> "error!"
Instead we must first convert the string to an Array
or an OrderedCollection
:
'abc' asArray collect: [ :ea | ea asciiValue ] >>> #(97 98 99)
Actually collect:
is not guaranteed to return a collection of exactly the same class as the receiver, but only the same species. In the case of an Interval
, the species is an Array
!
(1 to: 5) collect: [ :ea | ea * 2 ] >>> #(2 4 6 8 10)
Selecting and rejecting elements
The message select:
returns the elements of the receiver that satisfy a particular condition:
(2 to: 20) select: [ :each | each isPrime ] >>> #(2 3 5 7 11 13 17 19)
The message reject:
does the opposite:
(2 to: 20) reject: [ :each | each isPrime ] >>> #(4 6 8 9 10 12 14 15 16 18 20)
Identifying an element with detect:
The message detect:
returns the first element of the receiver that matches block argument.
'through' detect: [ :each | each isVowel ] >>> $o
The message detect:ifNone:
is a variant of the method detect:
. Its second block is evaluated when there is no element matching the block.
Smalltalk globals allClasses detect: [:each | '*cobol*' match: each asString] ifNone: [ nil ] >>> nil
Accumulating results with inject:into:
Functional programming languages often provide a higher-order function called fold or reduce to accumulate a result by applying some binary operator iteratively over all elements of a collection. In Pharo this is done by Collection>>inject:into:
.
The first argument is an initial value, and the second argument is a two-argument block which is applied to the result this far, and each element in turn.
A trivial application of inject:into:
is to produce the sum of a collection of numbers. In Pharo we could write this expression to sum the first 100 integers:
(1 to: 100) inject: 0 into: [ :sum :each | sum + each ] >>> 5050
Another example is the following one-argument block which computes factorials:
factorial := [ :n | (1 to: n) inject: 1 into: [ :product :each | product * each ] ]. factorial value: 10 >>> 3628800
Other messages
There are many other iterator messages. Just check the Collection
class.
count: The message count:
returns the number of elements satisfying a condition. The condition is represented as a boolean block.
Smalltalk globals allClasses count: [ :each | 'Collection*' match: each asString ] >>> 6
includes: The message includes:
checks whether the argument is contained in the collection.
| colors | colors := {Color white . Color yellow . Color blue . Color orange}. colors includes: Color blue. >>> true
anySatisfy: The message anySatisfy:
answers true if at least one element of the collection satisfies the condition represented by the argument.
colors anySatisfy: [ :c | c red > 0.5 ] >>> true