Skip to main content
Version: 23.3

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.

note

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.