Slag:Advanced ForEach

From Plasmaworks

Jump to: navigation, search


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.