I have been tooling around with ReactiveCocoa and I've come upon an interesting problem. I can envision any number of ugly, stateful solutions, but I'm pretty confident there is an elegant, functional way that's just not materializing in my head for whatever reason. Maybe you can help!
The input signal here is two part strings, like "<letter>,<number>"
. The desired sort rules are that for a given letter, the input values should appear at the output in the order of <number>
(i.e. A,2
should never appear before A,1
) and that across all letters <letter>
outputs should not violate alpha order. (i.e. no string starting with B
should appear until at least one string starting with A
has appeared.) Other than as dictated by those rules, the expectation is that things will arrive on the output in the order they are submitted to the input.
Consider the following code:
RACSubject* input = [RACSubject subject];
RACSignal* output = [input <SOME CHAIN OF SIGNAL FUNCTIONS>];
[output subscribeNext:^(id x) { NSLog(@"(%@)",x); }];
[input sendNext: @"A,2"]; // Expect no output
[input sendNext: @"B,4"]; // Expect no output
[input sendNext: @"B,2"]; // Expect no output
[input sendNext: @"B,1"]; // Expect no output
[input sendNext: @"A,1"]; // Expect output: (A,1) (A,2) (B,1) (B,2)
// Note: (A,1) (B,1) (B,2) (A,2) would *not* be right because A,2 appeared on the input before B,1
[input sendNext: @"C,1"]; // Expect output: (C,1)
[input sendNext: @"B,3"]; // Expect output: (B,3) (B,4)
[input sendNext: @"C,3"]; // Expect no output
[input sendNext: @"C,2"]; // Expect output: (C,2) (C,3)
Also output should be produced "eagerly". It's not useful if I have to wait until the input signal is completed before seeing output (unless, of course, the sorting rules dictate that that's the case, i.e. if A,1
came last)
Any ideas?
Writing this in an imperative way, you'd probably use some accumulator variables, then loop over the input values and manipulate the accumulators as necessary.
The closest parallel in functional programming is a scan (represented in ReactiveCocoa as -scanWithStart:reduce:
). A scan allows you to "thread" state through the stream and make use of it whenever a new input value arrives.
The result looks pretty similar to imperative accumulation, except that any mutation doesn't escape the scan block:
RACSignal *output = [[[[input
map:^(NSString *combo) {
NSArray *components = [combo componentsSeparatedByString:@","];
NSInteger number = [components[1] integerValue];
return RACTuplePack(components[0], @(number));
}]
// We need four state parameters:
// 1. The letter we're waiting for.
// 2. The number we're waiting for.
// 3. Values received that cannot be forwarded until a certain
// letter/number.
// 4. The values to forward at each step.
scanWithStart:RACTuplePack(@"A", @1, @[], @[]) reduce:^(RACTuple *state, RACTuple *letterAndNumber) {
NSString *waitingLetter = state[0];
NSNumber *waitingNumber = state[1];
NSArray *queuedValues = state[2];
// Enqueue this value until we're ready to send it (which may or may not
// occur on this step of the scan).
queuedValues = [queuedValues arrayByAddingObject:letterAndNumber];
if ([letterAndNumber.first isEqual:waitingLetter] && [letterAndNumber.second isEqual:waitingNumber]) {
// Determine the next letter and number.
waitingLetter = …;
waitingNumber = @(waitingNumber.integerValue + 1);
// Sort queuedValues lexically and numerically.
NSArray *forwardValues = …;
// We should no longer have any values queued, since we want to
// forward them all.
return RACTuplePack(waitingLetter, waitingNumber, @[], forwardValues);
} else {
// No values should escape the scan yet. Just pass on our queued
// values.
return RACTuplePack(waitingLetter, waitingNumber, queuedValues, @[]);
}
}]
map:^(RACTuple *state) {
// Convert the array of values into a signal.
NSArray *forwardValues = state.last;
return forwardValues.rac_sequence.signal;
}]
// Forward values from each inner signal in the correct, sorted order.
concat];
I've omitted some of the sorting logic for brevity, but that's pretty easy to fill in with the exact details of your algorithm.
As the question says that (A,1) (A,2) (B,1) (B,2) (C,1) (B,3) (B,4)
is a valid output, we'll find that we need to store the waiting number for each letter when you looking at the (C,1)
between (B,2)
and (B,3)
.
What's more, as the question says:
Other than as dictated by those rules, the expectation is that things will arrive on the output in the order they are submitted to the input.
So after forwarding some values, we may still have some queued values. Take the question's output as an example, we must still have (B,4)
queued for the coming (B,3)
after forwarding (C,1)
.
Here is a code example that RAC parts are almost the same as Justin's while I highlight the differences in comments:
Also, I post a full and runnable code example: http://d.pr/X59S/9p9bT58U.
RACSubject *input = [RACSubject subject];
RACSignal *output = [[[[input
map:^(NSString *combo) {
NSArray *components = [combo componentsSeparatedByString:@","];
NSInteger number = [components[1] integerValue];
return RACTuplePack(components[0], @(number));
}]
// !!!: DIFF
// We need there state parameters:
// 1. The letters and numbers we're waiting for.
// 2. Values received that cannot be forwarded until a certain
// letter/number.
// 3. The values to forward at each step.
scanWithStart:RACTuplePack(@{ @"A": @1 }, @[], @[]) reduce:^id(RACTuple *state, RACTuple *letterAndNumber) {
__block NSDictionary *waitingLettersAndNumbers = state[0];
__block NSArray *queuedValues = state[1];
// Enqueue this value until we're ready to send it (which may or may not
// occur on this step of the scan).
queuedValues = [queuedValues arrayByAddingObject:letterAndNumber];
char letterChar = [letterAndNumber.first characterAtIndex:0];
// !!!: DIFF
// (letter + 1, number) may be a valid output after forwarding (letter, number + 1)
if (
[[waitingLettersAndNumbers objectForKey:letterAndNumber.first] isEqualToNumber:letterAndNumber.second]
&&
(letterChar == 'A' || [[waitingLettersAndNumbers objectForKey:[NSString stringWithFormat:@"%c", letterChar - 1]] integerValue] > [letterAndNumber.second integerValue])
) {
NSMutableDictionary *mutableWaitingLettersAndNumbers = [NSMutableDictionary dictionaryWithDictionary:waitingLettersAndNumbers];
// Sort queuedValues lexically and numerically.
queuedValues = ...
// Determine the next letter and number.
NSMutableArray *forwardValues = [NSMutableArray array];
NSMutableArray *remindValues = [NSMutableArray array];
[queuedValues enumerateObjectsUsingBlock:^(RACTuple *tuple, NSUInteger idx, BOOL *stop) {
...
}];
queuedValues = [remindValues copy];
waitingLettersAndNumbers = [mutableWaitingLettersAndNumbers copy];
// !!!: DIFF
// After forwarding some values, we may still have some queued values.
return RACTuplePack(waitingLettersAndNumbers, queuedValues, forwardValues);
} else {
// No values should escape the scan yet. Just pass on our queued
// values.
return RACTuplePack(waitingLettersAndNumbers, queuedValues, @[]);
}
}]
map:^(RACTuple *state) {
// Convert the array of values into a signal.
NSArray *forwardValues = state.last;
return [forwardValues.rac_sequence signalWithScheduler:[RACScheduler immediateScheduler]];
}]
// Forward values from each inner signal in the correct, sorted order.
concat];
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With