package
0.0.1
Repository: https://github.com/barbell-math/util.git
Documentation: pkg.go.dev

# README

iter

A generic lazy iterator framework package that respects errors as values with reverse message passing for resource management.

https://github.com/barbell-math/util/blob/9948c0045f7246acb5c2827ea4b34cb4919fcae9/src/iter/example_test.go#L28-L39

//Example Output:
//Area is 1000.000000 Using step size: 0.000100
//Err is: <nil>

Design

This package takes advantage of the fact that functions can have methods in go. This allows an iterator to be defined as a simple function, as shown below.

https://github.com/barbell-math/util/blob/9948c0045f7246acb5c2827ea4b34cb4919fcae9/src/iter/Common.go#L14-L22 The iter type defined in this package.

Given this iterator type, it can have methods attached to it that can call on their relative 'object' (which would be a function in this case) to get values as needed. Each method then returns a new iterator, allowing the method calls to be chained together. The end result of this is a recursive, lazily evaluated iterator sequence.

There are three parts to any iterator chain, with the middle part being optional:

  1. Producer: The producer is responsible for creating a sequence of values to pass to the rest of the iterators. The source can be a slice, channel, or a single value.
  2. Intermediary: An Intermediary is responsible for taking it's parent iterators values, mutating and/or filtering them, and passing them down to it's child iterator.
  3. Consumer: The consumer is what collects all of the final values and either saves them or performs some other aggregate function with them.

Producers

Producers can be any function that returns an iterator. They are responsible for producing the sequence of values that the rest of the iterator chain consumes. There are several rules that a consumer must obey:

  1. Errors are returned from the producer.
  2. When an error is returned the value of the iterator element that is returned does not have to be valid.
  3. When an error is returned the continue flag must be set to false.
  4. A producer will only perform resource management when it receives the break action, not when it returns the initial error.

Intermediaries

Intermediaries sit between producers and consumers. They consume the values from a parent iterator and (potentially) pass that value to the consumer after (potentially) applying some transformation to the value. There are several rules that an intermediary must obey:

  1. Errors are propagated down to the consumer.
  2. When an error is returned the continue flag must be set to false.
  3. All break actions will be passed up to the producer. This allows resources to be destroyed in a top down fashion.
  4. If an intermediary produces a continue flag that tells the next iterator to stop, it should not clean up its parents or itself, but should simply return the flag to not continue. The consumer will start the destruction process once it sees the command to not continue.

Tip: Next is a very ubiquitous intermediary, most other intermediaries can be expressed using Next making them pseudo-intermediaries. By using this pattern all pseudo-intermediaries are abstracted away from the complex looping logic outlined above and do not need to worry about iterator feedback and message passing.

Consumers

Consumers are the final stage in a iterator sequence. Without a consumer a iterator chain will not be consumed due to the iterator chain being lazily evaluated. There are several rules that a consumer must obey:

  1. When an error is generated no further values should be consumed and the break action should be passed to the consumers parent iterator.
  2. When all elements have been consumed iteration should stop and the break action should be passed to the consumers parent iterator.
  3. All errors generated from a consumers parent iterator chain should be returned to the calling code.

Tip: ForEach is a very ubiquitous consumer. Most other consumers can be represented using ForEach, making them pseudo-consumers. By using this pattern all pseudo-consumers are abstracted away from the complex looping logic outlined above and do not need to worry about iterator feedback and message passing.

Reverse Message Passing

Each iterator chooses one of three actions to attach to the current value in the iterator sequence. These values are managed for each individual iterator, and are passed between the child and parent iterators.

  1. Continue: Signaling to 'accept' the current value and pass it along to the child iterator.
  2. Break: Signaling to ignore the current value and return the signal to stop iterating.
  3. Iterate: Signaling the current iterator to continue iterating and pull the next value in the iterator sequence.

Any iterator can produce an error or signal its child iterator to stop iterating. When this happens, the command to stop iterating must be passed all the way down to the consumer with no action being taken by any of the intermediaries. Recognizing that iteration should stop, the consumer must then call its parent iterator with the Break action. This action must be propagated all the way up to the producer without any intermediaries performing any action. Upon receiving this value the producer should perform it's resource management. Once done, the producer should return any errors and it's child iterator should then perform resource management. Each successive intermediary will then perform it's resource management once it's parent iterator is done until the consumer is reached. This allows for resources to be properly destroyed in a top-down fashion. This pattern of events is demonstrated in the image below.

Reverse Message Passing

Sudo Iterators

The intermediaries and consumers can be further sub-categorized:

  1. Pseudo: Any iterator that is expressed using another iterator. For intermediaries a it is common to use Next and for consumers it is common to use ForEach.

https://github.com/barbell-math/util/blob/9948c0045f7246acb5c2827ea4b34cb4919fcae9/src/iter/PseudoConsumer.go#L65-L77 Example pseudo-consumer

https://github.com/barbell-math/util/blob/9948c0045f7246acb5c2827ea4b34cb4919fcae9/src/iter/PseudoIntermediary.go#L21-L36 Example pseudo-intermediary

  1. Non-Pseudo: Any iterator that is not expressed using another iterator. For examples refer to the ForEach and Next functions.

Tip: If you are looking to extend this package and add more iterators, it is recommended that any new intermediary or consumer iterators are created using the non-pseudo iterators. This will reduce errors and time spent needlessly banging your head against a wall.

Benchmarking

Obviously, there will be overhead when using this package instead of using plain for loops. The example_test.go file not only showcases the example at the top of this readme, but contains benchmarks for three different scenarios. These scenarios are shown below for convenience.

Scenario 1: An over the top implementation using iterators

https://github.com/barbell-math/util/blob/9948c0045f7246acb5c2827ea4b34cb4919fcae9/src/iter/example_test.go#L28-L38

Scenario 2: Another implementation using iterators

https://github.com/barbell-math/util/blob/9948c0045f7246acb5c2827ea4b34cb4919fcae9/src/iter/example_test.go#L53-L59

Scenario 3: A basic for loop

https://github.com/barbell-math/util/blob/9948c0045f7246acb5c2827ea4b34cb4919fcae9/src/iter/example_test.go#L74-77

The benchmarks (gathered from the go benchmark utility) for the scenarios with various step sizes are shown below. Make of these results as you will.

ScenarioStep SizeTime
117815 ns/op
215930 ns/op
313706 ns/op
10.184634 ns/op
20.159650 ns/op
30.134406 ns/op
10.01765754 ns/op
20.01576928 ns/op
30.01352233 ns/op
10.0017526192 ns/op
20.0015810898 ns/op
30.0013460169 ns/op
10.000173463321 ns/op
20.000157991047 ns/op
30.000134179406 ns/op