Iterators
Iteration is the process of stepping through a sequence of values. An iterator is an entity that provides such a sequence of values, one at a time. In SenseTalk, lists, ranges and property lists can all be used as iterators. In addition, you can create your own custom iterator objects. There are several ways that you can step through each of the values supplied by an iterator and work with each one in turn.
Iterating Using Repeat With Each
When you need to work with each value supplied by an iterator and perform some actions with that value, use a repeat with each loop. For example, here's a loop that uses each value from a list:
repeat with each color in ["red", "orange", "yellow", "green"]
put "I think " & color & " is a very pretty color."
end repeat
To iterate over all of the values in a range, use repeat with each just as you would with a list:
repeat with each letter in "A" to "G"
repeat with each digit in 1 to 3
put letter & digit -- Puts "A1", "A2", "A3", "B1", "B2", ...
end repeat
end repeat
Iterating Using Each Expressions
When you need to process each value (or a selected set of values) from a list or other iterator to produce a list of result values, an each expression can be very effective. The result of an each expression is always a list. See Each Expressions.
Iterating Using NextValue
The nextValue
function will return the next sequential value from an iterator. When the sequence is exhausted and no more values are available from that iterator, the special constant value end
is returned. The currentValue
function will return the current value again without advancing the iteration.
put 100 to 200 by 50 into range
put range's nextValue --> 100
put range's nextValue --> 150
put range's currentValue --> 150
put range's nextValue --> 200
put range's nextValue --> ⓔ ⓝ ⓓ
Modifying Iteration Using CurrentIndex
Both lists and ranges have a currentIndex
property. When a list or range is first created, the value of the currentIndex
is zero. It is also reset to zero at the start of a repeat with each loop or an each expression for that iterator.
During iteration, the currentIndex
property is incremented each time a value is retrieved from the iterator. The iterator's currentIndex
can be accessed to determine the item number of the current value in the source list or range.
set foo to [a,b,c,d,e]
put foo's currentIndex & each item of foo --> [1a,2b,3c,4d,5e]
What's more, the currentIndex
can be changed to alter the course of iteration. For example, this repeat loop will only display the odd numbers in the range:
set myRange to 1..100
repeat with each item of myRange
put it
add 1 to myRange.currentIndex -- skips the next item
end repeat
Changing a List During Iteration
While iterating over the items in a list using repeat with each
, an each
expression, or the nextValue
function, SenseTalk uses the currentIndex
property of the list to keep track of the iteration. Whenever items are inserted, deleted, or replaced in a list, the currentIndex
property is adjusted appropriately to reflect the change. So it is possible to add, remove, or replace items in a list during iteration without causing problems.
Custom Iterators
Lists and ranges are the most common sources used as iterators. You can also create a custom iterator by making a script or object with a nextValue
handler that returns the next value in a sequence. The values returned can be anything, and the implementation may be as simple as returning the nextValue
from a list that is a property of the object, or may be the next result calculated from some internal state maintained by the object. Iterators of this type are sometimes called "generators" because they generate values one at a time as needed.
To be used as an iterator, there are a few rules an object must follow. First, it must have an objectType
property with a value of "iterator". This tells SenseTalk to call the object's nextValue
handler to obtain each value for a repeat with each loop or an each expression. Second, of course, it must have a nextValue
handler to supply each value in the sequence. The nextValue
handler should return the value end
when the end of the sequence has been reached and no more values are available. If it doesn't do this, a repeat loop should include an exit repeat statement to break out of the loop at some point or it will continue forever. An each
expression should not be used with an iterator that doesn't eventually return end
.
In addition to a nextValue
handler, a custom iterator may optionally also have a startIteration
handler. If present, this handler will be called before nextValue
is called by either a repeat with each
loop or an each
expression to allow the iterator to set up any initial conditions, or to reset its state following any earlier iteration.
Here is an example of a custom iterator that generates values from the Fibonacci sequence, in which each value is the sum of the two preceding values (note that although the Fibonacci sequence is infinite, this iterator only returns the values up to ten thousand, allowing it to be used with an each expression):
set FibonacciGen to {a:0, b:1, objectType:"iterator", script: {{
to handle nextValue
if my b is more than ten thousand then return end
set [my a, my b] to [my b, my a + my b]
return my a
end nextValue
}} }
set fib to a new FibonacciGen
repeat 10
insert fib's nextValue into firstTen
end repeat
put firstTen --> [1,1,2,3,5,8,13,21,34,55]
put fib.nextValue --> 89
put each item of fib --> [144,233,377,610,987,1597,2584,4181,6765]
Property Lists as Iterators
You also can use a property list as an iterator. When iterating in a repeat loop, the property list supplies each key and its value as a list of two items. An object that defines its own iteration (objects with an objectType of "iterator") use their own iteration; objects with a different objectType iterate over the [key,value] pairs for their properties.
set people to {Marty:12, June:16, Aaron:6, Grace:11}
repeat with each [name,age] in people
put !"[[name]] is [[age]] years old"
end repeat
When iterating over the properties of a property list outside of a repeat loop, nextKey
can be used to retrieve the next key, nextValue
to retrieve the next value, or nextKeyValue
to retrieve the next [key,value] pair. CurrentKey
, currentValue
, and currentKeyValue
can be used to obtain the corresponding value for the current property without advancing the iteration.
set people to {Marty:12, June:16, Aaron:6, Grace:11}
put people's nextKey --> "Aaron" (iteration starts with the first key alphabetically)
put people's nextValue --> "11" (advances to next key and gets its value)
put people's currentKey --> "Grace" (gets the current key without advancing)
put people's nextKeyValue --> [June, 16] (advances and returns the key and its value)
Passing an Iterator As a Parameter
An iterator can be passed as a parameter to another script or handler. It is passed with its current state intact, including the value of the currentIndex
property of a list or range, and any properties of a custom iterator. So if the local script has been using values one at a time from the iterator (by using nextValue
), the called script can continue using later values in the sequence.
Keep in mind that SenseTalk ordinarily makes copies of any parameter values, so the called script will receive its own copy of the iterator. To share the iterator so that both the local and the called scripts will be using a single sequence of values from the iterator, pass the iterator by reference. For example:
set takeANumber to 1..20
put takeANumber's nextValue --> 1
put takeANumber's nextValue --> 2
scriptThatTakesTwoNumbers @takeANumber // takes 3 and 4
put takeANumber's nextValue --> 5
Without the "@" to pass takeANumber by reference, the final call to takeANumber's nextValue would return 3 instead of 5. By passing a reference here, both scripts share the same sequence of values rather than splitting off a copy to the called script.
The is an iterator
operator can be used to test whether a value (such as a parameter that was received) can be iterated:
if param(1) is not an iterator then throw "An Iterator Is Required"
Restarting Iteration
To restart iteration over a range or list beginning with the first value again, set its currentIndex
to zero. The next time you access its nextValue
, the first value in the range or list will be returned.
set the currentIndex of sequence to zero -- reset iteration
Some custom iterators may provide a mechanism for starting iteration again, and some may not. A custom iterator that is designed to be reusable by repeat with each
loops and each
expressions should implement a startIteration
handler that can be called to reset it. Other custom iterators may implement a currentIndex
property that can be reset just as you would for a list or range. But neither of those is required in a basic iterator.
Assigned List Values
Lists in SenseTalk grow dynamically as items are inserted or values are assigned to items beyond the previous extent of the list. This allows values to be assigned to items that may be widely dispersed in a list, as in this example:
set list to["a","b"]
put "g" into item 7 of list
put "z" into item 26 of list
put list --Puts [a,b,,,,,g,,,,,,,,,,,,,,,,,,,z]
The list in this example has 26 items, although only 4 of them have assigned values. The other items are empty. Iterating over the items of this list will access all 26 items. In some cases it may be useful to be able to access only those list items that have explicitly been assigned a value. The nextAssignedValue
function does this. It works like the nextValue
function, but instead of simply incrementing the currentIndex
property by 1 and returning the value at that index, it advances the currentIndex
to the next item in the list that has an assigned value and returns that value. So, continuing the example above:
put list's nextAssignedValue --> a
put list's currentIndex --> 1
put list's nextAssignedValue --> b
put list's currentIndex --> 2
put list's nextAssignedValue --> g
put list's currentIndex --> 7
put list's nextAssignedValue --> z
put list's currentIndex --> 26
put list's nextAssignedValue --> ⓔ ⓝ ⓓ
Since both the nextValue
and nextAssignedValue
functions advance the currentIndex
property of the list, you can mix calls to the two functions to obtain either the next sequential value of the list (whether assigned or not) or the next assigned value of the list after the current index as needed.
An empty value is not the same as an unassigned value. If an empty value is stored into an item of a list, that empty value will be returned by the nextAssignedValue
function.