ahmedk92 / Blog

My blog, in Github Issues.
https://ahmedk92.github.io/Blog/
18 stars 4 forks source link

Clearing Stateful Subjects #23

Open ahmedk92 opened 4 years ago

ahmedk92 commented 4 years ago

Functional reactive programming (FRP) frameworks have a class of subjects that are stateful. That is, they store a value that can be queried, or sent immediately to new subscribers upon subscription. Combine's CurrentValueSubject and RxSwift's BehaviorSubject are good examples.

Sometimes we need to prevent new subscribers from immediately receiving the stored value and instead expect a new value. This is what can be called clearing or resetting the subject. Looking up this problem on the internet yields a solution that involves both the producer and the consumer agreeing on a special value that can be ignored. Luckily, Swift's optional can be leveraged to make this look more brief and tidy.

Usually, to prevent external mutation, subjects are an implementation detail to an exposed publisher/observable (in Combine/RxSwift terminologies). So, we can define our subject to be of Optional<T> where T is our output value type. Then, we use compactMap on the subject to expose the publisher/observable. This way we can send nil values to the subject to replace the current value while being filtered out by compactMap.

Combine Example:

import UIKit
import Combine

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        intProvider.experiment()
        cancellable = intProvider.publisher.sink { int in
            print(int)
        }
    }

    private let intProvider = IntProvider()
    private var cancellable: AnyCancellable?
}

class IntProvider {
    var publisher: AnyPublisher<Int, Never> {
        subject
        .compactMap({ $0 })
        .eraseToAnyPublisher()
    }
    func experiment() {
        subject.send(nil)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.subject.send(1)
        }
    }
    private let subject = CurrentValueSubject<Int?, Never>(0)
}

RxSwift Example:

import UIKit
import RxSwift

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        intProvider.experiment()
        disposable = intProvider.int.subscribe(onNext: { int in
            print(int)
        })
    }

    private let intProvider = IntProvider()
    private var disposable: Disposable?
}

class IntProvider {
    var int: Observable<Int> {
        subject.compactMap({ $0 })
    }
    func experiment() {
        subject.onNext(nil)
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.subject.onNext(1)
        }
    }
    private let subject = BehaviorSubject<Int?>(value: 0)
}

Both print:

1

not:

0
1