Sequence, push, pull
When I wrote last week about Sequence, I was not satisfied with the implementation of operations like itemAtIndex:, any:, or all:. These operations fully materialized the entire sequence, but technically didn’t need to do so. This was because I hadn’t decided whether the canonical representation of a sequence should be push-style (items returned by producer to consumer) or pull-style (items passed to consumer by producer), and because the implementations of those operations were agnostic to whether the sequence was a push- or pull-form, they couldn’t provide partial materialization of the sequence.
Consider itemAtIndex:. To support retrieval of a single item from a sequence by index, the code performing the look-up needs to materialize a sequence up until the requested index, but does not need to materialize it any further to provide the correct value. This partial materialization can be accomplished provided the consumer of sequence values can give some of indication to the producer of sequence values instructing the latter to abort.
This could be done with a pull-style sequence if the value-receiving consumer function returned a BOOL indicating if it wanted more. This strikes me as a bit more conceptually tricky than the equivalent solution for the pull-style sequence form: the consumer simply stops calling the value-returning producer function when it no longer wants values.
Settled.
I decided to go ahead implementing itemAtIndex: using a pull-style form. It looks like this:
- (id)itemAtIndex:(NSUInteger)index {
id (^next)() = impl();
id item = nil;
// iterates over items
for (NSUInteger i = 0; (item = next()); i++) {
if (i == index)
return item;
}
return nil;
}
Here, we have an impl function which returns an id (^)() block called next. That block is called index times to get the corresponding element. If the the next function returns nil, the sequence has ended, and the result of itemAtIndex: is nil.
Other operations which can short-circuit work similarly:
- (BOOL)any:(BOOL(^)(id))predicate {
id item = nil;
for (id (^next)() = impl(); (item = next());)
if (predicate(item))
return YES;
return NO;
}
- (BOOL)all:(BOOL(^)(id))predicate {
id item = nil;
BOOL evaluatedItem = NO;
for (id (^next)() = impl(); (item = next());) {
evaluatedItem = YES;
if (!predicate(item))
return NO;
}
return evaluatedItem;
}
In both these cases, the impl function is called at the start of the operation to retrieve the item-producing function. That function is called until the result of the operation can be determined.