loading...

Push vs Pull Signal

onmyway133 profile image Khoa Pham ・4 min read

Original post https://github.com/onmyway133/blog/issues/28

The idea of Signal may originate from Elm Reactivity, and it has now been widely adopted in iOS

I once asked What are examples of hot and cold signal in ReactiveCocoa?

Whether it is hot vs cold, Signal vs Signal Producer, Observable vs Enumerable, ... it's good to understand how it gets implemented, so that to have a good sense of how they work

Monad

Basically, Signal and its Result are just monads, which are thing that can be mapped and chained.

Signal makes use of deferred execution callback blocks, and push vs pull is just how the Signal updates its value and the order the callbacks are called

Execution callback block is that we pass a function to another function, and it will get called when appropriated

Sync vs Async

Monad can be in either sync or async mode. Sync is easier to understand, but async is somewhat you're already familiar and used in practice

Basically,

  • Sync: you get the returned value right away via return
  • Aync: you get the returned value via callback block

Here is an example of a simple function

// Sync
func sum(a: Int, b: Int) -> Int {
    return a + b
}

// Async
func sum(a: Int, b: Int, completion: Int -> Void) {
    // Assumed it is a very long task to get the result
    let result = a + b

    completion(result)
}

Here is an example of Event

// Sync
public func map<U>(@noescape f: T -> U) -> Event<U> {
    switch self {
    case let .Next(value):
        return .Next(value: f(value))
    case let .Failed(error):
        return .Failed(error: error)
    }
}

// Async
public func map<U>(f: (T, U -> Void) -> Void) -> ((Event<U> -> Void) -> Void) {
        return { g in   // g: Event<U> -> Void
            switch self {
            case let .Next(value):
                f(value) { transformedValue in  // transformedValue: U
                    g(.Next(value: transformedValue))
                }
            case let .Failed(error):
                g(.Failed(error: error))
            }
        }
    }

Push Signal

Take a look at my Push Signal, called Signal, it is like how Promise A+ Then works

Implementation

public final class Signal<T> {
    var event: Event<T>?
    var callbacks: [Event<T> -> Void] = []

    func notify() {
        guard let event = event else {
            return
        }

        callbacks.forEach { callback in
            callback(event)
        }
    }

    func update(event event: Event<T>) {
        dispatch_sync(lockQueue) {
            self.event = event
        }

        notify()
    }

    public func subscribe(f: Event<T> -> Void) -> Signal<T> {
        // Callback
        if let event = event {
            f(event)
        }

        callbacks.append(f)

        return self
    }

    public func map<U>(f: T -> U) -> Signal<U> {
        let signal = Signal<U>()

        subscribe { event in
            signal.update(event: event.map(f))
        }

        return signal
    }
}

Usage

let signal = Signal<String>()

signal.map { value in
        return value.characters.count
    }.subscribe { event in
        if case let .Next(value) = event {
            XCTAssert(value == 4)
        } else {
            XCTAssert(false)
        }
}

signal.sendNext("test")

Callbacks

Given a chained signals like this

A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)

  • The idea is we send event to the source signal, and it propagates events through via callbacks.
  • Triggered by sending event to the source signal.
  • We must keep A as it keeps the others around
  • We subscribe the last D
  • We send event to the first A
  • A 's callback gets called, it it in turn calls callback of B with the result of A 's map, then B 's callback calls C 's callback with the result of B 's flatMap, ...

Pull Signal

Take a look at my Pull Signal, called Future

Implementation

Here operation is a task, when called and completed, will notify its completion

public struct Future<T> {
    let operation: (Event<T> -> Void) -> Void

    public init(operation: (Event<T> -> Void) -> Void) {
        self.operation = operation
    }

    public func start(completion: Event<T> -> Void) {
        operation() { event in
            completion(event)
        }
    }

    public func map<U>(f: T -> U) -> Future<U> {
        return Future<U> { completion in
            self.start { event in
                completion(event.map(f))
            }
        }
    }
}

Usage

let _ = Future<String> { completion in
        // There is some work here
        completion(Event(value: "test"))
    }
    .map { value in
        value.characters.count
    }.start { event in
        if case let .Next(value) = event {
            XCTAssert(value == 4)
        } else {
            XCTAssert(false)
        }
    }

Callbacks

Given a chained signals like this

A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)

  • The idea is we subscribe to the final signal D, and it cause the previous signals to action.
  • Triggered by subscribing to the final signal.
  • We must keep D as it keeps the others around
  • We subscribe the last D
  • D 's operation actions, and it cause C 's operation to action, ... then A 's operation actions. It is in A that the task is performed (like fetching network, retrieving database, file access, heavy computation, ...) to get the result, and A 's completion gets called. Then A's completion calls B 's completion with the result mapped by B 's map, ... all the way to the subscriber 's completion block

Posted on by:

onmyway133 profile

Khoa Pham

@onmyway133

My apps https://onmyway133.com/apps/

Discussion

pic
Editor guide