Skip to content

Latest commit

 

History

History
519 lines (380 loc) · 16.3 KB

BasicOperators.md

File metadata and controls

519 lines (380 loc) · 16.3 KB

Basic Operators

This document explains some of the most common operators used in ReactiveCocoa, and includes examples demonstrating their use.

Note that “operators”, in this context, refers to functions that transform Signals and SignalProducers, not custom Swift operators. In other words, these are composable primitives provided by ReactiveCocoa for working with event streams.

This document will use the term “event stream” when dealing with concepts that apply to both Signal and SignalProducer. When the distinction matters, the types will be referred to by name.

Performing side effects with event streams

  1. Observation
  2. Injecting effects

Operator composition

  1. Lifting

Transforming event streams

  1. Mapping
  2. Filtering
  3. Aggregating

Combining event streams

  1. Combining latest values
  2. Zipping

Flattening event streams

  1. Merging
  2. Concatenating
  3. Switching to the latest

Working with errors

  1. Catching failures
  2. Failable transformations
  3. Retrying
  4. Mapping errors
  5. Promote

Performing side effects with event streams

Observation

Signals can be observed with the observe function.

signal.observe { event in
    switch event {
    case let .value(value):
        print("Value: \(value)")
    case let .failed(error):
        print("Failed: \(error)")
    case .completed:
        print("Completed")
    case .interrupted:
        print("Interrupted")
    }
}

Alternatively, callbacks for the value, failed, completed and interrupted events can be provided which will be called when a corresponding event occurs.

signal.observeValues { value in
    print("Value: \(value)")
}

signal.observeFailed { error in
    print("Failed: \(error)")
}

signal.observeCompleted {
    print("Completed")
}

signal.observeInterrupted {
    print("Interrupted")
}

Injecting effects

Side effects can be injected on an event stream with the on operator without actually subscribing to it.

let producer = signalProducer
    .on(starting: { 
        print("Starting")
    }, started: { 
        print("Started")
    }, event: { event in
        print("Event: \(event)")
    }, value: { value in
        print("Value: \(value)")
    }, failed: { error in
        print("Failed: \(error)")
    }, completed: { 
        print("Completed")
    }, interrupted: { 
        print("Interrupted")
    }, terminated: { 
        print("Terminated")
    }, disposed: { 
        print("Disposed")
    })

Note that it is not necessary to provide all parameters - all of them are optional, you only need to provide callbacks for the events you care about.

Note that nothing will be printed until producer is started (possibly somewhere else).

Operator composition

Lifting

Signal operators can be lifted to operate upon SignalProducers using the lift method.

This will create a new SignalProducer which will apply the given operator to every Signal created, just as if the operator had been applied to each produced Signal individually.

Transforming event streams

These operators transform an event stream into a new stream.

Mapping

The map operator is used to transform the values in an event stream, creating a new stream with the results.

let (signal, observer) = Signal<String, Never>.pipe()

signal
    .map { string in string.uppercased() }
    .observeValues { value in print(value) }

observer.send(value: "a")     // Prints A
observer.send(value: "b")     // Prints B
observer.send(value: "c")     // Prints C

Interactive visualisation of the map operator.

Filtering

The filter operator is used to only include values in an event stream that satisfy a predicate.

let (signal, observer) = Signal<Int, Never>.pipe()

signal
    .filter { number in number % 2 == 0 }
    .observeValues { value in print(value) }

observer.send(value: 1)     // Not printed
observer.send(value: 2)     // Prints 2
observer.send(value: 3)     // Not printed
observer.send(value: 4)     // prints 4

Interactive visualisation of the filter operator.

Aggregating

The reduce operator is used to aggregate a event stream’s values into a single combined value. Note that the final value is only sent after the input stream completes.

let (signal, observer) = Signal<Int, Never>.pipe()

signal
    .reduce(1) { $0 * $1 }
    .observeValues { value in print(value) }

observer.send(value: 1)     // nothing printed
observer.send(value: 2)     // nothing printed
observer.send(value: 3)     // nothing printed
observer.sendCompleted()   // prints 6

The collect operator is used to aggregate a event stream’s values into a single array value. Note that the final value is only sent after the input stream completes.

let (signal, observer) = Signal<Int, Never>.pipe()

signal
    .collect()
    .observeValues { value in print(value) }

observer.send(value: 1)     // nothing printed
observer.send(value: 2)     // nothing printed
observer.send(value: 3)     // nothing printed
observer.sendCompleted()   // prints [1, 2, 3]

Interactive visualisation of the reduce operator.

Combining event streams

These operators combine values from multiple event streams into a new, unified stream.

Combining latest values

The combineLatest function combines the latest values of two (or more) event streams.

The resulting stream will only send its first value after each input has sent at least one value. After that, new values on any of the inputs will result in a new value on the output.

let (numbersSignal, numbersObserver) = Signal<Int, Never>.pipe()
let (lettersSignal, lettersObserver) = Signal<String, Never>.pipe()

let signal = Signal.combineLatest(numbersSignal, lettersSignal)
signal.observeValues { next in print("Next: \(next)") }
signal.observeCompleted { print("Completed") }

numbersObserver.send(value: 0)      // nothing printed
numbersObserver.send(value: 1)      // nothing printed
lettersObserver.send(value: "A")    // prints (1, A)
numbersObserver.send(value: 2)      // prints (2, A)
numbersObserver.sendCompleted()  // nothing printed
lettersObserver.send(value: "B")    // prints (2, B)
lettersObserver.send(value: "C")    // prints (2, C)
lettersObserver.sendCompleted()  // prints "Completed"

The combineLatest(with:) operator works in the same way, but as an operator.

Interactive visualisation of the combineLatest operator.

Zipping

The zip function joins values of two (or more) event streams pair-wise. The elements of any Nth tuple correspond to the Nth elements of the input streams.

That means the Nth value of the output stream cannot be sent until each input has sent at least N values.

let (numbersSignal, numbersObserver) = Signal<Int, Never>.pipe()
let (lettersSignal, lettersObserver) = Signal<String, Never>.pipe()

let signal = Signal.zip(numbersSignal, lettersSignal)
signal.observeValues { next in print("Next: \(next)") }
signal.observeCompleted { print("Completed") }

numbersObserver.send(value: 0)      // nothing printed
numbersObserver.send(value: 1)      // nothing printed
lettersObserver.send(value: "A")    // prints (0, A)
numbersObserver.send(value: 2)      // nothing printed
numbersObserver.sendCompleted()  // nothing printed
lettersObserver.send(value: "B")    // prints (1, B)
lettersObserver.send(value: "C")    // prints (2, C) & "Completed"

The zipWith operator works in the same way, but as an operator.

Interactive visualisation of the zip operator.

Flattening event streams

The flatten operator transforms a stream-of-streams into a single stream - where values are forwarded from the inner stream in accordance with the provided FlattenStrategy. The flattened result becomes that of the outer stream type - i.e. a SignalProducer-of-SignalProducers or SignalProducer-of-Signals gets flattened to a SignalProducer, and likewise a Signal-of-SignalProducers or Signal-of-Signals gets flattened to a Signal.

To understand why there are different strategies and how they compare to each other, take a look at this example and imagine the column offsets as time:

let values = [
// imagine column offset as time
[ 1,    2,      3 ],
   [ 4,      5,     6 ],
         [ 7,     8 ],
]

let merge =
[ 1, 4, 2, 7,5, 3,8,6 ]

let concat = 
[ 1,    2,      3,4,      5,     6,7,     8]

let latest =
[ 1, 4,    7,     8 ]

Note, how the values interleave and which values are even included in the resulting array.

Merging

The .merge strategy immediately forwards every value of the inner event streams to the outer event stream. Any failure sent on the outer event stream or any inner event stream is immediately sent on the flattened event stream and terminates it.

let (lettersSignal, lettersObserver) = Signal<String, Never>.pipe()
let (numbersSignal, numbersObserver) = Signal<String, Never>.pipe()
let (signal, observer) = Signal<Signal<String, Never>, Never>.pipe()

signal.flatten(.merge).observeValues { print($0) }

observer.send(value: lettersSignal)
observer.send(value: numbersSignal)
observer.sendCompleted()

lettersObserver.send(value: "a")    // prints "a"
numbersObserver.send(value: "1")    // prints "1"
lettersObserver.send(value: "b")    // prints "b"
numbersObserver.send(value: "2")    // prints "2"
lettersObserver.send(value: "c")    // prints "c"
numbersObserver.send(value: "3")    // prints "3"

Interactive visualisation of the flatten(.merge) operator.

Concatenating

The .concat strategy is used to serialize events of the inner event streams. The outer event stream is started observed. Each subsequent event stream is not observed until the preceeding one has completed. Failures are immediately forwarded to the flattened event stream.

let (lettersSignal, lettersObserver) = Signal<String, Never>.pipe()
let (numbersSignal, numbersObserver) = Signal<String, Never>.pipe()
let (signal, observer) = Signal<Signal<String, Never>, Never>.pipe()

signal.flatten(.concat).observeValues { print($0) }

observer.send(value: lettersSignal)
observer.send(value: numbersSignal)
observer.sendCompleted()

numbersObserver.send(value: "1")    // nothing printed
lettersObserver.send(value: "a")    // prints "a"
lettersObserver.send(value: "b")    // prints "b"
numbersObserver.send(value: "2")    // nothing printed
lettersObserver.send(value: "c")    // prints "c"
lettersObserver.sendCompleted()     // nothing printed
numbersObserver.send(value: "3")    // prints "3"
numbersObserver.sendCompleted()     // nothing printed

Interactive visualisation of the flatten(.concat) operator.

Switching to the latest

The .latest strategy forwards only values or a failure from the latest input event stream.

let (lettersSignal, lettersObserver) = Signal<String, Never>.pipe()
let (numbersSignal, numbersObserver) = Signal<String, Never>.pipe()
let (signal, observer) = Signal<Signal<String, Never>, Never>.pipe()

signal.flatten(.latest).observeValues { print($0) }

observer.send(value: lettersSignal) // nothing printed
numbersObserver.send(value: "1")    // nothing printed
lettersObserver.send(value: "a")    // prints "a"
lettersObserver.send(value: "b")    // prints "b"
numbersObserver.send(value: "2")    // nothing printed
observer.send(value: numbersSignal) // nothing printed
lettersObserver.send(value: "c")    // nothing printed
numbersObserver.send(value: "3")    // prints "3"

Working with errors

These operators are used to handle failures that might occur on an event stream, or perform operations that might fail on an event stream.

Catching failures

The flatMapError operator catches any failure that may occur on the input event stream, then starts a new SignalProducer in its place.

let (signal, observer) = Signal<String, NSError>.pipe()
let producer = SignalProducer(signal)

let error = NSError(domain: "domain", code: 0, userInfo: nil)

producer
    .flatMapError { _ in SignalProducer<String, Never>(value: "Default") }
    .startWithValues { print($0) }


observer.send(value: "First")     // prints "First"
observer.send(value: "Second")    // prints "Second"
observer.send(error: error)       // prints "Default"

Failable transformations

SignalProducer.attempt(_:) allows you to turn a failable operation into an event stream. The attempt(_:) and attemptMap(_:) operators allow you to perform failable operations or transformations on an event stream.

let dictionaryPath = URL(fileURLWithPath: "/usr/share/dict/words")

// Create a `SignalProducer` that lazily attempts the closure
// whenever it is started
let data = SignalProducer.attempt { try Data(contentsOf: dictionaryPath) }

// Lazily apply a failable transformation
let json = data.attemptMap { try JSONSerialization.jsonObject(with: $0) }

json.startWithResult { result in
    switch result {
    case let .success(words):
        print("Dictionary as JSON:")
        print(words)
    case let .failure(error):
        print("Couldn't parse dictionary as JSON: \(error)")
    }
}

Retrying

The retry operator will restart the original SignalProducer on failure up to count times.

var tries = 0
let limit = 2
let error = NSError(domain: "domain", code: 0, userInfo: nil)
let producer = SignalProducer<String, NSError> { (observer, _) in
    tries += 1
    if tries <= limit {
        observer.send(error: error)
    } else {
        observer.send(value: "Success")
        observer.sendCompleted()
    }
}

producer
    .on(failed: {e in print("Failure")})    // prints "Failure" twice
    .retry(upTo: 2)
    .start { event in
        switch event {
        case let .value(next):
            print(next)                     // prints "Success"
        case let .failed(error):
            print("Failed: \(error)")
        case .completed:
            print("Completed")
        case .interrupted:
            print("Interrupted")
        }
}

If the SignalProducer does not succeed after count tries, the resulting SignalProducer will fail. E.g., if retry(1) is used in the example above instead of retry(2), "Failed: Error Domain=domain Code=0 "(null)"" will be printed instead of "Success".

Mapping errors

The mapError operator transforms the error of any failure in an event stream into a new error.

enum CustomError: String, Error {
    case foo = "Foo Error"
    case bar = "Bar Error"
    case other = "Other Error"
}

let (signal, observer) = Signal<String, NSError>.pipe()

signal
    .mapError { (error: NSError) -> CustomError in
        switch error.domain {
        case "com.example.foo":
            return .foo
        case "com.example.bar":
            return .bar
        default:
            return .other
        }
    }
    .observeFailed { error in
        print(error.rawValue)
}

observer.send(error: NSError(domain: "com.example.foo", code: 42, userInfo: nil))    // prints "Foo Error"

Promote

The promoteError operator promotes an event stream that does not generate failures into one that can.

let (numbersSignal, numbersObserver) = Signal<Int, Never>.pipe()
let (lettersSignal, lettersObserver) = Signal<String, NSError>.pipe()

numbersSignal
    .promoteError(NSError.self)
    .combineLatest(with: lettersSignal)

The given stream will still not actually generate failures, but this is useful because some operators to combine streams require the inputs to have matching error types.