Slag:Advanced ForEach
From Plasmaworks
Contents |
Using Custom Objects With ForEach
The standard way to have your own objects work with a forEach loop is to have your class incorporate the Readable<<ElementType>> aspect and define the method create_reader().reader<<ElementType>>. That method should return a new object that implements the Reader<<ElementType>> aspect. Finally, that new reader object should respond to has_another().Logical, peek().ElementType, and read().ElementType.
Here's an example of a Fibonacci class that's compatible with forEach.
class FibTest
method init:
forEach (n in FibonacciSequence(0,1,10)) println(n)
# prints: 0 1 1 2 3 5 8 13 21 34
println( FibonacciSequence(5,8,6) )
# prints: {5,8,13,21,34,56}
endClass
class FibonacciSequence( Int64 first, Int64 second, Int32 count ) : Readable<<Int64>>
METHODS
method create_reader.Reader<<Int64>>:
return FibonacciReader(this)
method to_String.String:
return this.to_List.to_String
endClass
class FibonacciReader : Reader<<Int64>>
PROPERTIES
previous, current : Int64
count : Int32
METHODS
method init( FibonacciSequence seq ):
current = seq.first
previous = seq.second - current
count = seq.count
method has_another.Logical:
return count?
method peek.Int64:
if (count == 0) throw NoNextValueError()
return current
method read.Int64:
if (count == 0) throw NoNextValueError()
local var next = previous + current
previous = current
current = next
--count
return previous
endClass
- Readable objects are designed to be read over and over again - if create_reader() is called on one two consecutive times, the reader object should return the same sequence of numbers both times.
- Reader objects are designed to be read only once. The general philosophy is that they are created, read from, and then discarded.
- A "forEach (n in readable_obj)" call translates into the code similar to the following at compile time:
local var reader = readable_obj.create_reader while (reader.has_another) local var n = reader.read ... endWhile
- A "forEach (n in reader_obj)" call translates into the code similar to the following at compile time:
while (reader_obj.has_another) local var n = reader_obj.read ... endWhile
Concurrent Modification Errors and RemoveCurrent
When working with forEach loops it's fairly easy to write code that generates a concurrent modification error. This happens when you add elements to or remove elements from a list while still iterating through it. For example:
forEach (index of active_list)
local Entity entity = active_list[index]
if (entity.is_dead)
active_list.remove(entity)
# This action causes a CME on the next iteration because the list structure
# has changed between one iteration and the next!
endIf
endForEach
Concurrent Modification Errors can be frustrating but they help guard against logic errors. Here are some design patterns to consider using if you want to add or remove list items while iterating.
- If you want to add items while iterating - and possibly remove others - you can use two lists. One list "alpha" contains your current bunch of items. Another list "beta" starts out empty. You copy any "surviving" alpha items as well as any new items to "beta", then swap the two lists. An example:
PROPERTIES alpha(), beta() : Entity[] ... forEach (entity in alpha) entity.update if (entity.still_alive) beta.add(entity) if (entity.firing) beta.add( Bullet() ) endForEach local var temp = alpha alpha = beta beta = temp beta.clear
- If you only want to remove items, you could read from a read position, write to a write position, and skip the writing whenever you don't want to save an item. After the loop is over you can discard the tail end of the list. Example:
local Int32 write_pos = 0 forEach (read_pos of list) local var entity = list[read_pos]; read_pos++ entity.update if (entity.still_alive) list[write_pos] = entity; write_pos++ endForEach list.discard( read_pos )
- However, this can be a little tedious. Slag's built-in removeCurrent command does the same thing as above and is much more convenient:
forEach (entity in list)
entity.update
if (not entity.still_alive) removeCurrent entity
endForEac
- Note that the removeCurrent command only works with lists, not with other types of readers and readable objects.