Previous | Table of Contents | Next |
Using blocks, it is easy to construct methods that implement more specialized control constructs. For this reason, the actual use of conventional do loops is fairly rare in Smalltalk programs. Instead, programmers typically use messages that invoke more specialized forms of iteration. For example, the message do: is used to iterate over each element on an array or any other type of data collection that supports it via polymorphism. This includes strings because they are defined as collections of characters. The message do: evaluates its argument block once for each element in the data collection that is the receiver of the message. The data elements are successively passed as the argument to the block. The following illustrates some examples that use do:. The first example simply sends a message to each element of an array. The second code fragment shows how do: can be used to count the number of occurrences of a specific character within a string:
customerArray do: [:each| each printMonthlyStatement]. n := 0. This is a test do: [:c| c = $s ifTrue: [n := n + 1]]. ^n 3 the character $s occurred three times
The message collect: is similar to do: in that it evaluates a block for each element of a data collection. However, unlike do:, collect: returns a new collection that is the same size as the original collection. Each element of the new collection is the object that is the result of evaluating the argument block using the corresponding element of the receiver collection as the argument. For example, the following expression creates an array containing the names of each customer from an array of customer objects:
nameArray := customerArray collect: [:element| element name]
The messages select: and reject: are used to create new collections consisting of those elements from the receiver collection that pass or fail a test that is specified by the argument block. The message select: includes in the result collection only those elements for which the argument block evaluates to true. The message reject: excludes elements for which the argument block evaluates to true. Consider these lines, for example:
John Q Public select: [:c| c isUppercase] JQP #(1 2 3 4 5 6 7 8 9) reject: [:n| n odd] #(2 4 6 8)
Multiple blocks can also be used to create control structures that combine iterative and conditional operations. For example, the message detect:ifNone: is used to search a collection for the first element that passes a test defined by the first argument block. However, if a matching element is not found, the second block argument is evaluated and its result is returned. This is used to provide a default value for situations where searches fail. Consider these lines, for example:
customerJohn := customerArray detect: [:cust| cust name = John Q Public] ifNone: [Customer create a default customer name: John Q Public address: Any Town customerNumber: 1].
Messages such as do:, select:, reject:, and collect: are reminiscent of Lisp map functions. Map functions typically take a list and a function as arguments and apply the function to each element of the list. Similarly, in Smalltalk the common iteration messages are sent to a collection and take a block as an argument. In modern Lisp dialects such as Scheme, the function argument to a map function is a lexical closure. A lexical closure is a function whose free variables have been bound within an active environment of variables. Smalltalk blocks are semantically equivalent to Scheme closures. This is the reason that blocks are sometimes also called block closures. It is important to understand that each evaluation of a block constructor creates a new and distinct block closure object with distinct variable bindings. When a block closure is created, the bindings of any references from the block to variables or arguments in the surrounding method (or any surrounding blocks) are fixed such that the block still references the current activation of those variables whenever the block closure is evaluated. The following method returns a block as its result:
Class Something Instance Method |
capture: arg
return a block that remembers the value passed into capture: |temp| temp:=arg. ^[temp] |
This method accepts an arbitrary object and returns a block that at some later time can be evaluated to return the original argument object. Consider these expressions, for example:
block1 := Something new capture: a string to capture in block 1. block1 value a string to capture in block 1
Additional invocations of the capture: method create additional block objects, each of which can return different values. The additional method invocations and block creations do not affect the values that were captured by any previously existing blocks, even though they were created by the same block constructor:
block2 := Something new capture: captured by block 2. block3 := Something new capture: block 3 holds me. block2 value captured by block 2 block1 value a string to capture in block 1 block3 value block 3 holds me
Each time the method capture: is invoked, a new local variable is created and bound to the name temp. The reference to temp from within the block is bound to that specific local variable. When the block object is returned from the method, its reference to the local variable causes the variable to continue to exist, even though the method invocation that created the variable has terminated. Because each block in the preceding example is created by a separate method invocation, each block references a different local variable named temp. In fact, the use of a temporary variable is not necessary in this example. The block could be code to directly return the value of the method argument, as its binding is also uniquely captured each time a block is created.
Previous | Table of Contents | Next |