Functional Reactive Programming (FRP): From Getting Started to 'Giving Up'—Illustrated RACSignal
Contents
- 1.Creating RACSignal
- 2.Subscribing to RACSignal
- 3.RACSignal Operations
####I. Creating RACSignal
1.Creating a unit signal
NSError *errorObject = [NSError errorWithDomain:@"Something wrong" code:500 userInfo:nil];
//Four basic creation methods
RACSignal *signal1 = [RACSignal return:@"Some Value"];
RACSignal *signal2 = [RACSignal error:errorObject];
RACSignal *signal3 = [RACSignal empty];
RACSignal *signal4 = [RACSignal never];
- Create dynamic signals
RACSignal *signal5 = [RACSignal createSignal:
^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendNext:@2];
[subscriber sendError:errorObject];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];
3.Obtain a signal through Cocoa bridging
RACSignal *signal6 = [view rac_signalForSelector:@selector(setFrame:)];
RACSignal *signal7 = [view rac_signalForControlEvents:UIControlEventTouchUpInside];
RACSignal *signal8 = [view rac_willDeallocSignal];
//KVO implementation principle
RACSignal *signal9 = RACObserve(view, backgroundColor);
- Obtained through signal transformation
RACSignal *signal10 = [signal1 map:^id(NSString *value) {
return [value substringFromIndex:1];
}];
- Obtained through sequence conversion
RACSequence *sequence = @[@"A", @"B", @"C"].rac_sequence;
//The signal11 obtained after this conversion is a signal that emits values very quickly
RACSignal *signal11 = sequence.signal;
####II. Subscribing to RACSignal
- Subscription method
[signal11 subscribeNext:^(id x) {
NSLog(@"next value is %@", x);
} error:^(NSError *error) {
NSLog(@"Ops! Get some error: %@", error);
} completed:^{
NSLog(@"It finished success");
}];
- Binding methods
//This is equivalent to KVO; each time signal produces a color, it assigns it to backgroundColor
RAC(view, backgroundColor) = signal10;
- Cocoa Bridging
[view rac_liftSelector:@selector(convertPoint:toView:)
withSignals:signal1, signal2, nil];
[view rac_liftSelector:@selector(convertRect:toView:)
withSignalsFromArray:@[signal3, signal4]];
[view rac_liftSelector:@selector(convertRect:toLayer:)
withSignalOfArguments:signal5];
It is worth noting: if the selector has a return value, then calling rac_liftSelector will produce a new signal. The new signal is the return value after invoking the selector.
- Execution Flow of the Subscription Process
RACSignal *signal = [RACSignal createSignal:
^RACDisposable *(id<RACSubscriber> subscriber)
{
1 [subscriber sendNext:@1];
2 [subscriber sendNext:@2];
3 [subscriber sendCompleted];
4 return [RACDisposable disposableWithBlock:^{
5 NSLog(@"dispose");
}];
}];
RACDisposable *disposable = [signal subscribeNext:^(id x) {
6 NSLog(@"next value is %@", x);
} error:^(NSError *error) {
7 NSLog(@"Ops! Get some error: %@", error);
} completed:^{
8 NSLog(@"It finished success");
}];
9 [disposable dispose];
If a signal is subscribed to, execution first enters the block where the signal is created. When execution reaches sendNext, sendCompleted, or sendError, control then moves to the subscriber’s corresponding subscribeNext, completed, or error block. The main execution flow still takes place inside the signal creation block; it only jumps to the subscriber’s corresponding block when the specific sendNext, sendCompleted, or sendError call is reached. After the subscriber’s corresponding block finishes executing, control returns to the signal’s block and continues executing downward. Finally, after the signal finishes executing, the disposable is executed. When the signal detects a disposable, it immediately invokes the RACDisposable block inside the signal creation logic.
Execution sequence: 1-6-2-6-3-8-4-5-9
The difference between RACStream and RACSignal is whether they return data or events.
There are three types of events in total: value, error, and completion.
A value may be an Objective-C object, a RACTuple, or even a RACSignal.
First, let’s talk about RACTuple. RACTuple is also a data type defined by RAC; it can be considered a simplified version of NSArray.
RACTuple *tuple = RACTuplePack(@1, @"haha");
id first = tuple.first;
id second = tuple.second;
id last = tuple.last;
id index1 = tuple[1];
RACTupleUnpack(NSNumber *num, NSString *str) = tuple;
There are two macros here. RACTuplePack can pack the following two objects into an RACTuple. RACTuple also supports subscript access. During use, the values are not of type id, so you need to cast the types explicitly. This is similar to the usage RACTupleUnpack(NSNumber *num, NSString *str).
Three. Various Operations on RACSignal
First, let’s define the legend.

1. Transformations on a Single Signal
- Operations on values
- Operations on quantity
- Operations on dimensions
- Operations on time intervals

- Operations on values
- Map operation
- MapReplace operation
- ReduceEach operation
- not operation
- and operation
- or operation
- reduceApply operation
- materialize operation
- dematerialize operation
(1) Map operation

RACSignal *signalA = @[@1, @2, @3, @4].rac_sequence.signal;
RACSignal *SignalB = [signalA map:^id(NSNumber *value) {
return @(value.integerValue * 2);
}];
If an Error is encountered during Map, the Error will not be mapped; it is returned directly as an Error.

(2) MapReplace operation

RACSignal *signalA = @[@1, @2, @3, @4].rac_sequence.signal;
RACSignal *signalB = [signalA map:^id(id value) {
return @8;
}]; // signalB is --8--8--8--8--|
RACSignal *signalC = [signalA mapReplace:@8];
// signalC is --8--8--8--8--| too.
(3) ReduceEach Operation

RACTuple *a = RACTuplePack(@1, @2);
RACTuple *b = RACTuplePack(@2, @3);
RACTuple *c = RACTuplePack(@3, @5);
RACSignal *signalA = @[a, b, c].rac_sequence.signal;
RACSignal *signalB = [signalA reduceEach:^id(NSNumber *first,
NSNumber *second) {
return @(first.integerValue + second.integerValue);
}];
Some concrete types of parameters can follow reduceEach.
(4) The not operation

RACSignal *signalA = @[@0, @1, @1, @0].rac_sequence.signal;
RACSignal *signalB = [signalA not];
(5) AND Operation

RACTuple *a = RACTuplePack(@0, @1);
RACTuple *b = RACTuplePack(@0, @0);
RACTuple *c = RACTuplePack(@1, @1);
RACTuple *d = RACTuplePack(@1, @0);
RACSignal *signalA = @[a, b, c, d].rac_sequence.signal;
RACSignal *signalB = [signalA and];
(6) or Operation

RACTuple *a = RACTuplePack(@0, @1);
RACTuple *b = RACTuplePack(@0, @0);
RACTuple *c = RACTuplePack(@1, @1);
RACTuple *d = RACTuplePack(@1, @0);
RACSignal *signalA = @[a, b, c, d].rac_sequence.signal;
RACSignal *signalB = [signalA or];
(7) reduceApply Operation
Here, a block is treated as the first of an RACTuple, while second and third are other parameters. When the reduceApply operation is executed, it takes the block from the first parameter and then passes the second and third parameters into that block.
(8) materialize Operation This operation converts ordinary Complete, Error, and Next events into ordinary value events and emits them.
(9) dematerialize OperationRACEvent has some values
RACEventTypeCompleted,
RACEventTypeError,
RACEventTypeNext
After using the dematerialize operation, you can convert the values above back into the corresponding events.
(10) Scan Operation

RACSignal *signalA = @[@1, @2, @3, @4].rac_sequence.signal;
RACSignal *signalB = [signalA scanWithStart:@0
reduce:^id(NSNumber *running,
NSNumber *next) {
return @(running.integerValue + next.integerValue);
}];
Compared with the Aggregate operation in the numeric operations below, the advantage here is obvious: each “scan” can produce a value.
- (RACSignal *)scanWithStart:(id)startingValue
reduceWithIndex:(id (^)(id running, id next, NSUInteger index))reduceBlock;
The one above is a variant of the scan function.
- Operations on item counts
- Filter operation
- Ignore / IgnoreValues operation
- DistinctUntilChanged operation
- Take operation
- Skip operation
- StartWith operation
- Repeat operation
- Retry operation
- Collect operation
- Aggregate operation
(1) Filter operation

RACSignal *signalA = @[@"ab", @"hello", @"ppp", @"0"].rac_sequence.signal;
RACSignal *signalB = [signalA filter:^BOOL(NSString *value) {
return value.length > 2;
}];
(2) Ignore / IgnoreValues Operations
Ignore Operation

RACSignal *signalA = @[@1, @2, @1, @3].rac_sequence.signal;
RACSignal *signalB = [signalA filter:^BOOL(id value) {
return ![@1 isEqual:value];
}];
RACSignal *signalC = [signalA ignore:@1];
IgnoreValues Operation
After performing this operation, the new signal no longer has any values; only Error and Complete events remain.

RACSignal *signalA = @[@1, @2, @1, @3].rac_sequence.signal;
RACSignal *signalB = [signalA ignoreValues];
(3) DistinctUntilChanged Operation This operation is essentially a deduplication operation.

RACSignal *signalA = @[@1,@1,@2,@2,@3].rac_sequence.signal;
RACSignal *signalB = [signalA distinctUntilChanged];
(4) Take Operation

RACSignal *signalA = @[@1, @2, @3].rac_sequence.signal;
RACSignal *signalB = [signalA take:2];
(5) Skip Operation

RACSignal *signalA = @[@1, @2, @3].rac_sequence.signal;
RACSignal *signalB = [signalA skip:2];
take and Skip variant methods
- (RACSignal *)takeLast:(NSUInteger)count;
- (RACSignal *)takeUntilBlock:(BOOL (^)(id x))predicate;
- (RACSignal *)takeWhileBlock:(BOOL (^)(id x))predicate;
- (RACSignal *)skipUntilBlock:(BOOL (^)(id x))predicate;
- (RACSignal *)skipWhileBlock:(BOOL (^)(id x))predicate;
A predicate is a rule passed in to determine whether to take something or skip it.
(6) StartWith operation

RACSignal *signalA = @[@"ab", @"hello", @"app", @"1"].rac_sequence.signal;
RACSignal *signalB = [signalA startWith:@"Start"];
(7) Repeat Operation

RACSignal *signalA = @[@"ab", @"hello"].rac_sequence.signal;
RACSignal *signalB = [signalA repeat];
(8) Retry Operation
In some network requests, if we wrap the network fetch as a signal, then when the request fails, an error is emitted indicating that the network cannot connect. At this point, the client may need to reconnect.

RACSignal *signalA = @[@"ab"].rac_sequence.signal;
RACSignal *signalB = [signalA retry:2];
The value after Retry is the number of retries
This is where side-effecting operations come into play.
RACSignal *signalA = @[@"ab", @"hello", @"ppp", @"0"].rac_sequence.signal;
RACSignal *signalB = [signalA map:^id(id value) {
// do some thing;
return value;
}];
RACSignal *signalC = [signalA doNext:^(id x) {
// do some thing;
}];
Side-effect operations are actions that do not affect the value itself but perform some additional work, such as playing an animation, printing text on the screen, showing a pop-up, and so on.
The following convenience methods are available:
- (RACSignal *)doError:(void (^)(NSError *error))block;
- (RACSignal *)doCompleted:(void (^)(void))block;
- (RACSignal *)initially:(void (^)(void))block;
- (RACSignal *)finally:(void (^)(void))block;
(9) Collect Operation

RACSignal *signalA = @[@"ab", @"hello", @"ppp", @"0"].rac_sequence.signal;
RACSignal *signalB = [signalA collect];
(10) Aggregate Operation This function is actually a “fold function”

RACSignal *signalA = @[@1, @2, @3, @4].rac_sequence.signal;
RACSignal *signalB = [signalA aggregateWithStart:@0
reduce:^id(NSNumber *running,
NSNumber *next) {
return @(running.integerValue + next.integerValue);
}];
It is worth noting that this function only returns when it terminates; if it does not terminate, it will never return. If it encounters a Repeat signal, it will never return. aggregateWithStart has an initial value. The drawback is that you cannot get intermediate values while it is changing; you only get a value at the end. Compared with the Scan operation among value operations, Scan is still the better choice.
- (RACSignal *)aggregateWithStart:(id)start
reduceWithIndex:(id (^)(id running, id next, NSUInteger index))reduceBlock;
- (RACSignal *)aggregateWithStartFactory:(id (^)(void))startFactory
reduce:(id (^)(id running, id next))reduceBlock;
This is a variant of the aggregate function.
- Operations on time intervals
- Unit-interval time signal
- Delay operation
- Throttle operation
(1) Unit-interval time signal
+ (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler;
+ (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler withLeeway:(NSTimeInterval)leeway;
The two methods above return a time signal at intervals of interval. A signal is emitted every interval.
(2) Delay Operation
Delays the source signal by 1 second before sending it.

RACSignal *signalB = [signalA delay:1];
(3) Throttle Operation (Throttling Operation)

RACSignal *signalB = [signalA throttle:2];
The value after throttle is the interval in seconds. If no new value arrives within that interval, it emits the previously observed value. For example, in the case above, after 1 is emitted, it waits for 2 seconds. If 2 arrives within those 2 seconds, it keeps listening for another 2 seconds. If 3 arrives within those 2 seconds, it continues listening for another 2 seconds. If no other value arrives within that period, it emits 3. Following the same logic, it will emit 5 and 6.
A common scenario where we use this function in development is search.
When a user is typing rapidly in a search box, sending a network request on every single keystroke would waste bandwidth and is unnecessary. This is where throttle comes in. If the user stops typing for 1 second, then we can send the network request.
- (RACSignal *)throttle:(NSTimeInterval)interval valuesPassingTest:(BOOL (^)(id next))predicate;
- (RACSignal *)bufferWithTime:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler;
These are variant functions of Throttle.
####2. Combining Multiple Signals
- 1.Concat Combination Operation
- 2.Merge Combination Operation
- 3.Zip Combination Operation
- 4.CombineLatest Combination Operation
- 5.Sample Combination Operation
- 6.TakeUntil / TakeUntilReplacement Combination Operation
1.Concat Combination Operation

RACSignal *signalA = @[@1, @2, @3, @4, @5].rac_sequence.signal;
RACSignal *signalB = @[@6, @7].rac_sequence.signal;
RACSignal *signalC = [signalA concat:signalB];
How Concat works: it subscribes to A first, and only after A has completed will it continue to subscribe to B. At this point, you can see that B has already completed long ago. This also illustrates that defining a signal and subscribing to a signal are separate operations.
If A emits an error, then B has nothing to do with it.

If B emits an error, then C will propagate that error.

- Merge Composition Operation

RACSignal *signalA = @[@1, @2, @3, @4, @5].rac_sequence.signal;
RACSignal *signalB = @[@6, @7].rac_sequence.signal;
RACSignal *signalC = [signalA merge:signalB];
RACSignal *signalC = [RACSignal merge:@[signalA, signalB]];
RACSignal *signalC = [RACSignal merge:RACTuplePack(signalA, signalB)];
If SignalA and SignalB are two threads, then SignalC will move between the two threads to send events. In the example shown above, 1, 2, 3, and 5 are emitted on thread A, while 4, 6, and 7 are emitted on thread B.
In real-world development, Merge may be used in scenarios like the following:
RACSignal *appearSignal = [[self rac_signalForSelector:@selector(viewDidAppear:)]
mapReplace:@YES];
RACSignal *disappearSignal = [[self rac_signalForSelector:@selector(viewWillDisappear:)]
mapReplace:@NO];
RACSignal *activeSignal = [RACSignal merge:@[appearSignal, disappearSignal]];
The activeSignal signal indicates whether the app is in the foreground.
3.Zip Combination Operation

RACSignal *signalA = @[@1, @2, @3, @5].rac_sequence.signal;
RACSignal *signalB = @[@4, @6, @7].rac_sequence.signal;
RACSignal *signalC = [signalA zipWith:signalB];
RACSignal *signalC = [RACSignal zip:@[signalA, signalB]];
RACSignal *signalC = [RACSignal zip:RACTuplePack(signalA, signalB)];
Pay attention to the color of the RACTuple in the diagram. Termination is controlled by whichever sequence is shorter. As for when to send, send at the time of whichever one arrives later. For (1, 4), 4 arrives later, so the RACTuple is sent when 4 arrives; for (2, 6), 6 arrives later, so the RACTuple is sent when 6 arrives. For (3, 7), 3 arrives later, so the RACTuple is sent when 3 arrives.
- CombineLatest Combination Operation

RACSignal *signalA = @[@1, @2, @3].rac_sequence.signal;
RACSignal *signalB = @[@4, @6, @7].rac_sequence.signal;
RACSignal *signalC = [signalA combineLatestWith:signalB];
RACSignal *signalC = [RACSignal combineLatest:@[signalA, signalB]];
RACSignal *signalC = [RACSignal combineLatest:RACTuplePack(signalA, signalB)];
Always combine with the latest value and return it as the new value. Termination is controlled by whoever is responsible for controlling it.
+ (RACSignal *)combineLatest:(id<NSFastEnumeration>)signals reduce:(id (^)())reduceBlock;
+ (RACSignal *)zip:(id<NSFastEnumeration>)streams reduce:(id (^)())reduceBlock;
These are convenience operations resembling syntactic sugar for the corresponding combineLatest and Zip. Each simply appends a Reduce afterward.
- Sample Combination Operation

RACSignal *signalC = [signalA sample:signalB];
Whichever ends first, the resulting signal ends along with it.

If an error occurs, this function acts like a shutter. It will still capture the Error and return it.
6.TakeUntil / TakeUntilReplacement Combination Operations

RACSignal *signalC = [signalA takeUntil:signalB];
When B occurs, terminate A.

RACSignal *signalC = [signalA takeUntilReplacement:signalB];
When B occurs, terminate A and then immediately follow it with B.
