Advanced ForEach

 

Using Custom Objects With ForEach | Concurrent Modification Errors

 

 

Using Custom Objects With ForEach

The standard way to have your own objects work as the data for 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.