| # Design Guidelines |
| |
| This document contains guidelines for projects that want to make use of |
| ReactiveCocoa. The content here is heavily inspired by the [Rx Design |
| Guidelines](http://blogs.msdn.com/b/rxteam/archive/2010/10/28/rx-design-guidelines.aspx). |
| |
| This document assumes basic familiarity |
| with the features of ReactiveCocoa. The [Framework Overview][] is a better |
| resource for getting up to speed on the functionality provided by RAC. |
| |
| **[The RACSequence contract](#the-racsequence-contract)** |
| |
| 1. [Evaluation occurs lazily by default](#evaluation-occurs-lazily-by-default) |
| 1. [Evaluation blocks the caller](#evaluation-blocks-the-caller) |
| 1. [Side effects occur only once](#side-effects-occur-only-once) |
| |
| **[The RACSignal contract](#the-racsignal-contract)** |
| |
| 1. [Signal events are serialized](#signal-events-are-serialized) |
| 1. [Subscription will always occur on a scheduler](#subscription-will-always-occur-on-a-scheduler) |
| 1. [Errors are propagated immediately](#errors-are-propagated-immediately) |
| 1. [Side effects occur for each subscription](#side-effects-occur-for-each-subscription) |
| 1. [Subscriptions are automatically disposed upon completion or error](#subscriptions-are-automatically-disposed-upon-completion-or-error) |
| 1. [Disposal cancels in-progress work and cleans up resources](#disposal-cancels-in-progress-work-and-cleans-up-resources) |
| |
| **[Best practices](#best-practices)** |
| |
| 1. [Use descriptive declarations for methods and properties that return a signal](#use-descriptive-declarations-for-methods-and-properties-that-return-a-signal) |
| 1. [Indent stream operations consistently](#indent-stream-operations-consistently) |
| 1. [Use the same type for all the values of a stream](#use-the-same-type-for-all-the-values-of-a-stream) |
| 1. [Avoid retaining streams for too long](#avoid-retaining-streams-for-too-long) |
| 1. [Process only as much of a stream as needed](#process-only-as-much-of-a-stream-as-needed) |
| 1. [Deliver signal events onto a known scheduler](#deliver-signal-events-onto-a-known-scheduler) |
| 1. [Switch schedulers in as few places as possible](#switch-schedulers-in-as-few-places-as-possible) |
| 1. [Make the side effects of a signal explicit](#make-the-side-effects-of-a-signal-explicit) |
| 1. [Share the side effects of a signal by multicasting](#share-the-side-effects-of-a-signal-by-multicasting) |
| 1. [Debug streams by giving them names](#debug-streams-by-giving-them-names) |
| 1. [Avoid explicit subscriptions and disposal](#avoid-explicit-subscriptions-and-disposal) |
| 1. [Avoid using subjects when possible](#avoid-using-subjects-when-possible) |
| |
| **[Implementing new operators](#implementing-new-operators)** |
| |
| 1. [Prefer building on RACStream methods](#prefer-building-on-racstream-methods) |
| 1. [Compose existing operators when possible](#compose-existing-operators-when-possible) |
| 1. [Avoid introducing concurrency](#avoid-introducing-concurrency) |
| 1. [Cancel work and clean up all resources in a disposable](#cancel-work-and-clean-up-all-resources-in-a-disposable) |
| 1. [Do not block in an operator](#do-not-block-in-an-operator) |
| 1. [Avoid stack overflow from deep recursion](#avoid-stack-overflow-from-deep-recursion) |
| |
| ## The RACSequence contract |
| |
| [RACSequence][] is a _pull-driven_ stream. Sequences behave similarly to |
| built-in collections, but with a few unique twists. |
| |
| ### Evaluation occurs lazily by default |
| |
| Sequences are evaluated lazily by default. For example, in this sequence: |
| |
| ```objc |
| NSArray *strings = @[ @"A", @"B", @"C" ]; |
| RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) { |
| return [str stringByAppendingString:@"_"]; |
| }]; |
| ``` |
| |
| … no string appending is actually performed until the values of the sequence are |
| needed. Accessing `sequence.head` will perform the concatenation of `A_`, |
| accessing `sequence.tail.head` will perform the concatenation of `B_`, and so |
| on. |
| |
| This generally avoids performing unnecessary work (since values that are never |
| used are never calculated), but means that sequence processing [should be |
| limited only to what's actually |
| needed](#process-only-as-much-of-a-stream-as-needed). |
| |
| Once evaluated, the values in a sequence are memoized and do not need to be |
| recalculated. Accessing `sequence.head` multiple times will only do the work of |
| one string concatenation. |
| |
| If lazy evaluation is undesirable – for instance, because limiting memory usage |
| is more important than avoiding unnecessary work – the |
| [eagerSequence][RACSequence] property can be used to force a sequence (and any |
| sequences derived from it afterward) to evaluate eagerly. |
| |
| ### Evaluation blocks the caller |
| |
| Regardless of whether a sequence is lazy or eager, evaluation of any part of |
| a sequence will block the calling thread until completed. This is necessary |
| because values must be synchronously retrieved from a sequence. |
| |
| If evaluating a sequence is expensive enough that it might block the thread for |
| a significant amount of time, consider creating a signal with |
| [-signalWithScheduler:][RACSequence] and using that instead. |
| |
| ### Side effects occur only once |
| |
| When the block passed to a sequence operator involves side effects, it is |
| important to realize that those side effects will only occur once per value |
| – namely, when the value is evaluated: |
| |
| ```objc |
| NSArray *strings = @[ @"A", @"B", @"C" ]; |
| RACSequence *sequence = [strings.rac_sequence map:^(NSString *str) { |
| NSLog(@"%@", str); |
| return [str stringByAppendingString:@"_"]; |
| }]; |
| |
| // Logs "A" during this call. |
| NSString *concatA = sequence.head; |
| |
| // Logs "B" during this call. |
| NSString *concatB = sequence.tail.head; |
| |
| // Does not log anything. |
| NSString *concatB2 = sequence.tail.head; |
| |
| RACSequence *derivedSequence = [sequence map:^(NSString *str) { |
| return [@"_" stringByAppendingString:str]; |
| }]; |
| |
| // Still does not log anything, because "B_" was already evaluated, and the log |
| // statement associated with it will never be re-executed. |
| NSString *concatB3 = derivedSequence.tail.head; |
| ``` |
| |
| ## The RACSignal contract |
| |
| [RACSignal][] is a _push-driven_ stream with a focus on asynchronous event |
| delivery through _subscriptions_. For more information about signals and |
| subscriptions, see the [Framework Overview][]. |
| |
| ### Signal events are serialized |
| |
| A signal may choose to deliver its events on any thread. Consecutive events are |
| even allowed to arrive on different threads or schedulers, unless explicitly |
| [delivered onto a particular |
| scheduler](#deliver-signal-events-onto-a-known-scheduler). |
| |
| However, RAC guarantees that no two signal events will ever arrive concurrently. |
| While an event is being processed, no other events will be delivered. The |
| senders of any other events will be forced to wait until the current event has |
| been handled. |
| |
| Most notably, this means that the blocks passed to |
| [-subscribeNext:error:completed:][RACSignal] do not need to be synchronized with |
| respect to each other, because they will never be invoked simultaneously. |
| |
| ### Subscription will always occur on a scheduler |
| |
| To ensure consistent behavior for the `+createSignal:` and `-subscribe:` |
| methods, each [RACSignal][] subscription is guaranteed to take place on |
| a valid [RACScheduler][]. |
| |
| If the subscriber's thread already has a [+currentScheduler][RACScheduler], |
| scheduling takes place immediately; otherwise, scheduling occurs as soon as |
| possible on a background scheduler. Note that the main thread is always |
| associated with the [+mainThreadScheduler][RACScheduler], so subscription will |
| always be immediate there. |
| |
| See the documentation for [-subscribe:][RACSignal] for more information. |
| |
| ### Errors are propagated immediately |
| |
| In RAC, `error` events have exception semantics. When an error is sent on |
| a signal, it will be immediately forwarded to all dependent signals, causing the |
| entire chain to terminate. |
| |
| [Operators][RACSignal+Operations] whose primary purpose is to change |
| error-handling behavior – like `-catch:`, `-catchTo:`, or `-materialize` – are |
| obviously not subject to this rule. |
| |
| ### Side effects occur for each subscription |
| |
| Each new subscription to a [RACSignal][] will trigger its side effects. This |
| means that any side effects will happen as many times as subscriptions to the |
| signal itself. |
| |
| Consider this example: |
| ```objc |
| __block int aNumber = 0; |
| |
| // Signal that will have the side effect of incrementing `aNumber` block |
| // variable for each subscription before sending it. |
| RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) { |
| aNumber++; |
| [subscriber sendNext:@(aNumber)]; |
| [subscriber sendCompleted]; |
| return nil; |
| }]; |
| |
| // This will print "subscriber one: 1" |
| [aSignal subscribeNext:^(id x) { |
| NSLog(@"subscriber one: %@", x); |
| }]; |
| |
| // This will print "subscriber two: 2" |
| [aSignal subscribeNext:^(id x) { |
| NSLog(@"subscriber two: %@", x); |
| }]; |
| ``` |
| |
| Side effects are repeated for each subscription. The same applies to |
| [stream][RACStream] and [signal][RACSignal+Operations] operators: |
| |
| ```objc |
| __block int missilesToLaunch = 0; |
| |
| // Signal that will have the side effect of changing `missilesToLaunch` on |
| // subscription. |
| RACSignal *processedSignal = [[RACSignal |
| return:@"missiles"] |
| map:^(id x) { |
| missilesToLaunch++; |
| return [NSString stringWithFormat:@"will launch %d %@", missilesToLaunch, x]; |
| }]; |
| |
| // This will print "First will launch 1 missiles" |
| [processedSignal subscribeNext:^(id x) { |
| NSLog(@"First %@", x); |
| }]; |
| |
| // This will print "Second will launch 2 missiles" |
| [processedSignal subscribeNext:^(id x) { |
| NSLog(@"Second %@", x); |
| }]; |
| ``` |
| |
| To suppress this behavior and have multiple subscriptions to a signal execute |
| its side effects only once, a signal can be |
| [multicasted](#share-the-side-effects-of-a-signal-by-multicasting). |
| |
| Side effects can be insidious and produce problems that are difficult to |
| diagnose. For this reason it is suggested to |
| [make side effects explicit](#make-the-side-effects-of-a-signal-explicit) when |
| possible. |
| |
| ### Subscriptions are automatically disposed upon completion or error |
| |
| When a [subscriber][RACSubscriber] is sent a `completed` or `error` event, the |
| associated subscription will automatically be disposed. This behavior usually |
| eliminates the need to manually dispose of subscriptions. |
| |
| See the [Memory Management][] document for more information about signal |
| lifetime. |
| |
| ### Disposal cancels in-progress work and cleans up resources |
| |
| When a subscription is disposed, manually or automatically, any in-progress or |
| outstanding work associated with that subscription is gracefully cancelled as |
| soon as possible, and any resources associated with the subscription are cleaned |
| up. |
| |
| Disposing of the subscription to a signal representing a file upload, for |
| example, would cancel any in-flight network request, and free the file data from |
| memory. |
| |
| ## Best practices |
| |
| The following recommendations are intended to help keep RAC-based code |
| predictable, understandable, and performant. |
| |
| They are, however, only guidelines. Use best judgement when determining whether |
| to apply the recommendations here to a given piece of code. |
| |
| ### Use descriptive declarations for methods and properties that return a signal |
| |
| When a method or property has a return type of [RACSignal][], it can be |
| difficult to understand the signal's semantics at a glance. |
| |
| There are three key questions that can inform a declaration: |
| |
| 1. Is the signal _hot_ (already activated by the time it's returned to the |
| caller) or _cold_ (activated when subscribed to)? |
| 1. Will the signal include zero, one, or more values? |
| 1. Does the signal have side effects? |
| |
| **Hot signals without side effects** should typically be properties instead of |
| methods. The use of a property indicates that no initialization is needed before |
| subscribing to the signal's events, and that additional subscribers will not |
| change the semantics. Signal properties should usually be named after events |
| (e.g., `textChanged`). |
| |
| **Cold signals without side effects** should be returned from methods that have |
| noun-like names (e.g., `-currentText`). A method declaration indicates that the |
| signal might not be kept around, hinting that work is performed at the time of |
| subscription. If the signal sends multiple values, the noun should be pluralized |
| (e.g., `-currentModels`). |
| |
| **Signals with side effects** should be returned from methods that have |
| verb-like names (e.g., `-logIn`). The verb indicates that the method is not |
| idempotent and that callers must be careful to call it only when the side |
| effects are desired. If the signal will send one or more values, include a noun |
| that describes them (e.g., `-loadConfiguration`, `-fetchLatestEvents`). |
| |
| ### Indent stream operations consistently |
| |
| It's easy for stream-heavy code to become very dense and confusing if not |
| properly formatted. Use consistent indentation to highlight where chains of |
| streams begin and end. |
| |
| When invoking a single method upon a stream, no additional indentation is |
| necessary (block arguments aside): |
| |
| ```objc |
| RACStream *result = [stream startWith:@0]; |
| |
| RACStream *result2 = [stream map:^(NSNumber *value) { |
| return @(value.integerValue + 1); |
| }]; |
| ``` |
| |
| When transforming the same stream multiple times, ensure that all of the |
| steps are aligned. Complex operators like [+zip:reduce:][RACStream] or |
| [+combineLatest:reduce:][RACSignal+Operations] may be split over multiple lines |
| for readability: |
| |
| ```objc |
| RACStream *result = [[[RACStream |
| zip:@[ firstStream, secondStream ] |
| reduce:^(NSNumber *first, NSNumber *second) { |
| return @(first.integerValue + second.integerValue); |
| }] |
| filter:^ BOOL (NSNumber *value) { |
| return value.integerValue >= 0; |
| }] |
| map:^(NSNumber *value) { |
| return @(value.integerValue + 1); |
| }]; |
| ``` |
| |
| Of course, streams nested within block arguments should start at the natural |
| indentation of the block: |
| |
| ```objc |
| [[signal |
| then:^{ |
| @strongify(self); |
| |
| return [[self |
| doSomethingElse] |
| catch:^(NSError *error) { |
| @strongify(self); |
| [self presentError:error]; |
| |
| return [RACSignal empty]; |
| }]; |
| }] |
| subscribeCompleted:^{ |
| NSLog(@"All done."); |
| }]; |
| ``` |
| |
| ### Use the same type for all the values of a stream |
| |
| [RACStream][] (and, by extension, [RACSignal][] and [RACSequence][]) allows |
| streams to be composed of heterogenous objects, just like Cocoa collections do. |
| However, using different object types within the same stream complicates the use |
| of operators and |
| puts an additional burden on any consumers of that stream, who must be careful to |
| only invoke supported methods. |
| |
| Whenever possible, streams should only contain objects of the same type. |
| |
| ### Avoid retaining streams for too long |
| |
| Retaining any [RACStream][] longer than it's needed will cause any dependencies |
| to be retained as well, potentially keeping memory usage much higher than it |
| would be otherwise. |
| |
| A [RACSequence][] should be retained only for as long as the `head` of the |
| sequence is needed. If the head will no longer be used, retain the `tail` of the |
| node instead of the node itself. |
| |
| See the [Memory Management][] guide for more information on object lifetime. |
| |
| ### Process only as much of a stream as needed |
| |
| As well as [consuming additional |
| memory](#avoid-retaining-streams-for-too-long), unnecessarily |
| keeping a stream or [RACSignal][] subscription alive can result in increased CPU |
| usage, as unnecessary work is performed for results that will never be used. |
| |
| If only a certain number of values are needed from a stream, the |
| [-take:][RACStream] operator can be used to retrieve only that many values, and |
| then automatically terminate the stream immediately thereafter. |
| |
| Operators like `-take:` and [-takeUntil:][RACSignal+Operations] automatically propagate cancellation |
| up the stack as well. If nothing else needs the rest of the values, any |
| dependencies will be terminated too, potentially saving a significant amount of |
| work. |
| |
| ### Deliver signal events onto a known scheduler |
| |
| When a signal is returned from a method, or combined with such a signal, it can |
| be difficult to know which thread events will be delivered upon. Although |
| events are [guaranteed to be serial](#signal-events-are-serialized), sometimes |
| stronger guarantees are needed, like when performing UI updates (which must |
| occur on the main thread). |
| |
| Whenever such a guarantee is important, the [-deliverOn:][RACSignal+Operations] |
| operator should be used to force a signal's events to arrive on a specific |
| [RACScheduler][]. |
| |
| ### Switch schedulers in as few places as possible |
| |
| Notwithstanding the above, events should only be delivered to a specific |
| [scheduler][RACScheduler] when absolutely necessary. Switching schedulers can |
| introduce unnecessary delays and cause an increase in CPU load. |
| |
| Generally, the use of [-deliverOn:][RACSignal+Operations] should be restricted |
| to the end of a signal chain – e.g., before subscription, or before the values |
| are bound to a property. |
| |
| ### Make the side effects of a signal explicit |
| |
| As much as possible, [RACSignal][] side effects should be avoided, because |
| subscribers may find the [behavior of side |
| effects](#side-effects-occur-for-each-subscription) unexpected. |
| |
| However, because Cocoa is predominantly imperative, it is sometimes useful to |
| perform side effects when signal events occur. Although most [RACStream][] and |
| [RACSignal][RACSignal+Operations] operators accept arbitrary blocks (which can |
| contain side effects), the use of `-doNext:`, `-doError:`, and `-doCompleted:` |
| will make side effects more explicit and self-documenting: |
| |
| ```objc |
| NSMutableArray *nexts = [NSMutableArray array]; |
| __block NSError *receivedError = nil; |
| __block BOOL success = NO; |
| |
| RACSignal *bookkeepingSignal = [[[valueSignal |
| doNext:^(id x) { |
| [nexts addObject:x]; |
| }] |
| doError:^(NSError *error) { |
| receivedError = error; |
| }] |
| doCompleted:^{ |
| success = YES; |
| }]; |
| |
| RAC(self, value) = bookkeepingSignal; |
| ``` |
| |
| ### Share the side effects of a signal by multicasting |
| |
| [Side effects occur for each |
| subscription](#side-effects-occur-for-each-subscription) by default, but there |
| are certain situations where side effects should only occur once – for example, |
| a network request typically should not be repeated when a new subscriber is |
| added. |
| |
| The `-publish` and `-multicast:` operators of [RACSignal][RACSignal+Operations] |
| allow a single subscription to be shared to any number of subscribers by using |
| a [RACMulticastConnection][]: |
| |
| ```objc |
| // This signal starts a new request on each subscription. |
| RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { |
| AFHTTPRequestOperation *operation = [client |
| HTTPRequestOperationWithRequest:request |
| success:^(AFHTTPRequestOperation *operation, id response) { |
| [subscriber sendNext:response]; |
| [subscriber sendCompleted]; |
| } |
| failure:^(AFHTTPRequestOperation *operation, NSError *error) { |
| [subscriber sendError:error]; |
| }]; |
| |
| [client enqueueHTTPRequestOperation:operation]; |
| return [RACDisposable disposableWithBlock:^{ |
| [operation cancel]; |
| }]; |
| }]; |
| |
| // Starts a single request, no matter how many subscriptions `connection.signal` |
| // gets. This is equivalent to the -replay operator, or similar to |
| // +startEagerlyWithScheduler:block:. |
| RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]]; |
| [connection connect]; |
| |
| [connection.signal subscribeNext:^(id response) { |
| NSLog(@"subscriber one: %@", response); |
| }]; |
| |
| [connection.signal subscribeNext:^(id response) { |
| NSLog(@"subscriber two: %@", response); |
| }]; |
| ``` |
| |
| ### Debug streams by giving them names |
| |
| Every [RACStream][] has a `name` property to assist with debugging. A stream's |
| `-description` includes its name, and all operators provided by RAC will |
| automatically add to the name. This usually makes it possible to identify |
| a stream from its default name alone. |
| |
| For example, this snippet: |
| |
| ```objc |
| RACSignal *signal = [[[RACObserve(self, username) |
| distinctUntilChanged] |
| take:3] |
| filter:^(NSString *newUsername) { |
| return [newUsername isEqualToString:@"joshaber"]; |
| }]; |
| |
| NSLog(@"%@", signal); |
| ``` |
| |
| … would log a name similar to `[[[RACObserve(self, username)] -distinctUntilChanged] |
| -take: 3] -filter:`. |
| |
| Names can also be manually applied by using [-setNameWithFormat:][RACStream]. |
| |
| [RACSignal][] also offers `-logNext`, `-logError`, |
| `-logCompleted`, and `-logAll` methods, which will automatically log signal |
| events as they occur, and include the name of the signal in the messages. This |
| can be used to conveniently inspect a signal in real-time. |
| |
| ### Avoid explicit subscriptions and disposal |
| |
| Although [-subscribeNext:error:completed:][RACSignal] and its variants are the |
| most basic way to process a signal, their use can complicate code by |
| being less declarative, encouraging the use of side effects, and potentially |
| duplicating built-in functionality. |
| |
| Likewise, explicit use of the [RACDisposable][] class can quickly lead to |
| a rat's nest of resource management and cleanup code. |
| |
| There are almost always higher-level patterns that can be used instead of manual |
| subscriptions and disposal: |
| |
| * The [RAC()][RAC] or [RACChannelTo()][RACChannelTo] macros can be used to bind |
| a signal to a property, instead of performing manual updates when changes |
| occur. |
| * The [-rac_liftSelector:withSignals:][NSObject+RACLifting] method can be used |
| to automatically invoke a selector when one or more signals fire. |
| * Operators like [-takeUntil:][RACSignal+Operations] can be used to |
| automatically dispose of a subscription when an event occurs (like a "Cancel" |
| button being pressed in the UI). |
| |
| Generally, the use of built-in [stream][RACStream] and |
| [signal][RACSignal+Operations] operators will lead to simpler and less |
| error-prone code than replicating the same behaviors in a subscription callback. |
| |
| ### Avoid using subjects when possible |
| |
| [Subjects][] are a powerful tool for bridging imperative code |
| into the world of signals, but, as the "mutable variables" of RAC, they can |
| quickly lead to complexity when overused. |
| |
| Since they can be manipulated from anywhere, at any time, subjects often break |
| the linear flow of stream processing and make logic much harder to follow. They |
| also don't support meaningful |
| [disposal](#disposal-cancels-in-progress-work-and-cleans-up-resources), which |
| can result in unnecessary work. |
| |
| Subjects can usually be replaced with other patterns from ReactiveCocoa: |
| |
| * Instead of feeding initial values into a subject, consider generating the |
| values in a [+createSignal:][RACSignal] block instead. |
| * Instead of delivering intermediate results to a subject, try combining the |
| output of multiple signals with operators like |
| [+combineLatest:][RACSignal+Operations] or [+zip:][RACStream]. |
| * Instead of using subjects to share results with multiple subscribers, |
| [multicast](#share-the-side-effects-of-a-signal-by-multicasting) a base |
| signal instead. |
| * Instead of implementing an action method which simply controls a subject, use |
| a [command][RACCommand] or |
| [-rac_signalForSelector:][NSObject+RACSelectorSignal] instead. |
| |
| When subjects _are_ necessary, they should almost always be the "base" input |
| for a signal chain, not used in the middle of one. |
| |
| ## Implementing new operators |
| |
| RAC provides a long list of built-in operators for [streams][RACStream] and |
| [signals][RACSignal+Operations] that should cover most use cases; however, RAC |
| is not a closed system. It's entirely valid to implement additional operators |
| for specialized uses, or for consideration in ReactiveCocoa itself. |
| |
| Implementing a new operator requires a careful attention to detail and a focus |
| on simplicity, to avoid introducing bugs into the calling code. |
| |
| These guidelines cover some of the common pitfalls and help preserve the |
| expected API contracts. |
| |
| ### Prefer building on RACStream methods |
| |
| [RACStream][] offers a simpler interface than [RACSequence][] and [RACSignal][], |
| and all stream operators are automatically applicable to sequences and signals |
| as well. |
| |
| For these reasons, new operators should be implemented using only [RACStream][] |
| methods whenever possible. The minimal required methods of the class, including |
| `-bind:`, `+zipWith:`, and `-concat:`, are quite powerful, and many tasks can |
| be accomplished without needing anything else. |
| |
| If a new [RACSignal][] operator needs to handle `error` and `completed` events, |
| consider using the [-materialize][RACSignal+Operations] method to bring the |
| events into the stream. All of the events of a materialized signal can be |
| manipulated by stream operators, which helps minimize the use of non-stream |
| operators. |
| |
| ### Compose existing operators when possible |
| |
| Considerable thought has been put into the operators provided by RAC, and they |
| have been validated through automated tests and through their real world use in |
| other projects. An operator that has been written from scratch may not be as |
| robust, or might not handle a special case that the built-in operators are aware |
| of. |
| |
| To minimize duplication and possible bugs, use the provided operators as much as |
| possible in a custom operator implementation. Generally, there should be very |
| little code written from scratch. |
| |
| ### Avoid introducing concurrency |
| |
| Concurrency is an extremely common source of bugs in programming. To minimize |
| the potential for deadlocks and race conditions, operators should not |
| concurrently perform their work. |
| |
| Callers always have the ability to subscribe or deliver events on a specific |
| [RACScheduler][], and RAC offers powerful ways to [parallelize |
| work][Parallelizing Independent Work] without making operators unnecessarily |
| complex. |
| |
| ### Cancel work and clean up all resources in a disposable |
| |
| When implementing a signal with the [+createSignal:][RACSignal] method, the |
| provided block is expected to return a [RACDisposable][]. This disposable |
| should: |
| |
| * As soon as it is convenient, gracefully cancel any in-progress work that was |
| started by the signal. |
| * Immediately dispose of any subscriptions to other signals, thus triggering |
| their cancellation and cleanup code as well. |
| * Release any memory or other resources that were allocated by the signal. |
| |
| This helps fulfill [the RACSignal |
| contract](#disposal-cancels-in-progress-work-and-cleans-up-resources). |
| |
| ### Do not block in an operator |
| |
| Stream operators should return a new stream more-or-less immediately. Any work |
| that the operator needs to perform should be part of evaluating the new stream, |
| _not_ part of the operator invocation itself. |
| |
| ```objc |
| // WRONG! |
| - (RACSequence *)map:(id (^)(id))block { |
| RACSequence *result = [RACSequence empty]; |
| for (id obj in self) { |
| id mappedObj = block(obj); |
| result = [result concat:[RACSequence return:mappedObj]]; |
| } |
| |
| return result; |
| } |
| |
| // Right! |
| - (RACSequence *)map:(id (^)(id))block { |
| return [self flattenMap:^(id obj) { |
| id mappedObj = block(obj); |
| return [RACSequence return:mappedObj]; |
| }]; |
| } |
| ``` |
| |
| This guideline can be safely ignored when the purpose of an operator is to |
| synchronously retrieve one or more values from a stream (like |
| [-first][RACSignal+Operations]). |
| |
| ### Avoid stack overflow from deep recursion |
| |
| Any operator that might recurse indefinitely should use the |
| `-scheduleRecursiveBlock:` method of [RACScheduler][]. This method will |
| transform recursion into iteration instead, preventing a stack overflow. |
| |
| For example, this would be an incorrect implementation of |
| [-repeat][RACSignal+Operations], due to its potential to overflow the call stack |
| and cause a crash: |
| |
| ```objc |
| - (RACSignal *)repeat { |
| return [RACSignal createSignal:^(id<RACSubscriber> subscriber) { |
| RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; |
| |
| __block void (^resubscribe)(void) = ^{ |
| RACDisposable *disposable = [self subscribeNext:^(id x) { |
| [subscriber sendNext:x]; |
| } error:^(NSError *error) { |
| [subscriber sendError:error]; |
| } completed:^{ |
| resubscribe(); |
| }]; |
| |
| [compoundDisposable addDisposable:disposable]; |
| }; |
| |
| return compoundDisposable; |
| }]; |
| } |
| ``` |
| |
| By contrast, this version will avoid a stack overflow: |
| |
| ```objc |
| - (RACSignal *)repeat { |
| return [RACSignal createSignal:^(id<RACSubscriber> subscriber) { |
| RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; |
| |
| RACScheduler *scheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler]; |
| RACDisposable *disposable = [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) { |
| RACDisposable *disposable = [self subscribeNext:^(id x) { |
| [subscriber sendNext:x]; |
| } error:^(NSError *error) { |
| [subscriber sendError:error]; |
| } completed:^{ |
| reschedule(); |
| }]; |
| |
| [compoundDisposable addDisposable:disposable]; |
| }]; |
| |
| [compoundDisposable addDisposable:disposable]; |
| return compoundDisposable; |
| }]; |
| } |
| ``` |
| |
| [Framework Overview]: FrameworkOverview.md |
| [Memory Management]: MemoryManagement.md |
| [NSObject+RACLifting]: ../ReactiveCocoaFramework/ReactiveCocoa/NSObject+RACLifting.h |
| [NSObject+RACSelectorSignal]: ../ReactiveCocoaFramework/ReactiveCocoa/NSObject+RACSelectorSignal.h |
| [RAC]: ../ReactiveCocoaFramework/ReactiveCocoa/RACSubscriptingAssignmentTrampoline.h |
| [RACChannelTo]: ../ReactiveCocoaFramework/ReactiveCocoa/RACKVOChannel.h |
| [RACCommand]: ../ReactiveCocoaFramework/ReactiveCocoa/RACCommand.h |
| [RACDisposable]: ../ReactiveCocoaFramework/ReactiveCocoa/RACDisposable.h |
| [RACEvent]: ../ReactiveCocoaFramework/ReactiveCocoa/RACEvent.h |
| [RACMulticastConnection]: ../ReactiveCocoaFramework/ReactiveCocoa/RACMulticastConnection.h |
| [RACObserve]: ../ReactiveCocoaFramework/ReactiveCocoa/NSObject+RACPropertySubscribing.h |
| [RACScheduler]: ../ReactiveCocoaFramework/ReactiveCocoa/RACScheduler.h |
| [RACSequence]: ../ReactiveCocoaFramework/ReactiveCocoa/RACSequence.h |
| [RACSignal]: ../ReactiveCocoaFramework/ReactiveCocoa/RACSignal.h |
| [RACSignal+Operations]: ../ReactiveCocoaFramework/ReactiveCocoa/RACSignal+Operations.h |
| [RACStream]: ../ReactiveCocoaFramework/ReactiveCocoa/RACStream.h |
| [RACSubscriber]: ../ReactiveCocoaFramework/ReactiveCocoa/RACSubscriber.h |
| [Subjects]: FrameworkOverview.md#subjects |
| [Parallelizing Independent Work]: ../README.md#parallelizing-independent-work |