Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to convert to/from AnyPublisher<Void, Never> and AnyPublisher<Never, Never>?

Tags:

swift

combine

I'm wrapping async requests in Combine publishers so they can easily be used across different pipelines.

A consumer might hold on to these publishers as follows:

struct Dependencies {
  var loadImageRequest: AnyPublisher<UIImage, Never>
  var saveToDatabaseRequest: AnyPublisher<Void, Never>
  var saveToUserDefaultsRequest: AnyPublisher<Never, Never>
}

Two of the more common types of requests are:

  1. Fire and forget (complete immediately): For example, saving a value to User Defaults, could be modeled as a fire and forget. So far it seems like AnyPublisher<Never, Never> is a good way to express this type. This can easily be constructed via Empty<Never, Never>(completeImmediately: true).
  2. Fire, wait, and ignore result: An example of this is saving a value to the database (and ignoring the result), but still wanting to wait until the save is complete before continuing the pipeline. I've been using AnyPublisher<Void, Never> to model these request types. An easy way to construct these is via a Future<Void, Never>() { promise in promise(.success(()))}.

Both of these have the common theme of ignoring the result. Therefore, when handing these off to the consumers, it's sometimes useful to convert between these two data types: AnyPublisher<Never, Never> and AnyPublisher<Void, Never>.

There are three potential ways to convert between the two:

  1. Never -> Void, complete immediately

    One way to convert this is with some forced casting:

    let neverPublisher: AnyPublisher<Never, Never> = ...
    let voidPublisher: AnyPublisher<Void, Never> = neverPublisher
      .map { _ in () }
      .append(Just(()))
      .eraseToAnyPublisher()
    
  2. Void -> Never, wait until Void completes

    Since there is a bulit-in operator for this one, the conversion is easy:

    let voidPublisher: AnyPublisher<Void, Never> = ...
    let neverPublisher: AnyPublisher<Never, Never> = voidPublisher
      .ignoreOutput()
      .eraseToAnyPublisher()
    
  3. Void -> Never, complete immediately

    I'm not sure the best way to do this conversion. The solution I came up with has two major downsides: using handleEvents, and needing cancellables defined somewhere:

    let cancellables: Set<AnyCancellable> = []
    let voidPublisher: AnyPublisher<Void, Never> = ...
    let neverPublisher: AnyPublisher<Never, Never> = Empty<Never, Never>(completeImmediately: true)
      .handleEvents(receiveCompletion: { _ in voidPublisher.sink(receiveValue: { _ in }).store(in: &cancellables) })
      .eraseToAnyPublisher()
    

Questions:

  1. Is there a better way to do conversion #1 (Never -> Void, complete immediately) without needing to invoke both map and append? (E.g. similar to how ignoreOutput was used to solve the second conversion?)
  2. Is there a better way to do the third conversion (Void -> Never, complete immediately) which doesn't need cancellables?
like image 238
Senseful Avatar asked Jul 12 '20 03:07

Senseful


1 Answers

I haven't found a more elegant way to do #2.

Here is a better #3 (Void -> Never, complete immediately) solution:

let voidPublisher: AnyPublisher<Void, Never> = ...
let neverPublisher: AnyPublisher<Never, Never> = Publishers.Merge(Just<Void>(()), saveToDatabase)
  .first()
  .ignoreOutput()
  .eraseToAnyPublisher()

And here is the timing Playground I used to ensure everything is functioning as expected:

import Combine
import Foundation

let delaySec: TimeInterval = 0.1
let halfDelaySec: TimeInterval = delaySec / 2
let halfDelayMicroSeconds: useconds_t = useconds_t(halfDelaySec * 1_000_000)
let sleepBufferMicroSeconds: useconds_t = useconds_t(0.01 * 1_000_000)

var cancellables = [AnyCancellable]()
var output: [String] = []

func performVoidAction(voidPublisher: AnyPublisher<Void, Never>) {
  voidPublisher
    .handleEvents(
      receiveCompletion: { _ in output.append("performVoidAction - completion") },
      receiveCancel: { output.append("performVoidAction - cancel") })
    .sink(receiveValue: { output.append("performVoidAction - sink") })
    .store(in: &cancellables)
}

func performNeverAction(neverPublisher: AnyPublisher<Never, Never>) {
  neverPublisher
    .handleEvents(
      receiveCompletion: { _ in output.append("performNeverAction - completion") },
      receiveCancel: { output.append("performNeverAction - cancel") })
    .sink(receiveValue: { _ in output.append("performNeverAction - sink") })
    .store(in: &cancellables)
}

func makeSaveToDatabasePublisher() -> AnyPublisher<Void, Never> {
  Deferred { _saveToDatabase() }.eraseToAnyPublisher()
}

func makeSaveToUserDefaultsPublisher() -> AnyPublisher<Never, Never> {
  Deferred { _saveToUserDefaults() }.eraseToAnyPublisher()
}

// --->(Void)|
// AnyPublisher<Void, Never> wraps an API that does something and finishes upon completion.
private func _saveToDatabase() -> AnyPublisher<Void, Never> {
  return Future<Void, Never> { promise in
    output.append("saving to database")
    DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: DispatchTime.now() + delaySec) {
      output.append("saved to database")
      promise(.success(()))
    }
  }.eraseToAnyPublisher()
}

// |
// AnyPublisher<Never, Never> wraps an API that does something and completes immediately, it does not wait for completion.
private func _saveToUserDefaults() -> AnyPublisher<Never, Never> {
  output.append("saved to user defaults")
  return Empty<Never, Never>(completeImmediately: true)
    .eraseToAnyPublisher()
}

func assert(_ value: Bool) -> String {
  value ? "✅" : "❌"
}

// tests
assert(output.isEmpty)

var saveToDatabase = makeSaveToDatabasePublisher()
assert(output.isEmpty, "It should not fire the action yet.")

// verify database save, first time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)

// verify database save, second time
performVoidAction(voidPublisher: saveToDatabase)
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty)

var saveToUserDefaults = makeSaveToUserDefaultsPublisher()
assert(output.isEmpty, "It should not fire the action yet.")

// verify user defaults save, first time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output

// verify user defaults save, second time
performNeverAction(neverPublisher: saveToUserDefaults)
assert(!output.isEmpty && output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output

// MARK: - Problem: AnyPublisher<Never, Never> -> AnyPublisher<Void, Never>

// MARK: Solution 1
// `|` ➡️ `(Void)|`

performVoidAction(
  voidPublisher: saveToUserDefaults
    .map { _ in () }
    .append(Just(()))
    .eraseToAnyPublisher())
assert(output.removeFirst() == "saved to user defaults")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - sink")
assert(!output.isEmpty && output.removeFirst() == "performVoidAction - completion")
assert(output.isEmpty) // 'perform never action' should never be output"


// MARK: - Problem: AnyPublisher<Void, Never> -> AnyPublisher<Never, Never>

// MARK: Solution 2 (Wait)
// `--->(Void)|` ➡️ `--->|`

performNeverAction(
  neverPublisher: saveToDatabase.ignoreOutput().eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(output.isEmpty)


// MARK: Solution 3 (No wait)
// `--->(Void)|` ➡️ `|`

performNeverAction(
  neverPublisher: Publishers.Merge(Just<Void>(()), saveToDatabase)
    .first()
    .ignoreOutput()
    .eraseToAnyPublisher())
assert(!output.isEmpty && output.removeFirst() == "performNeverAction - completion")
assert(!output.isEmpty && output.removeFirst() == "saving to database")
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(output.isEmpty)
usleep(halfDelayMicroSeconds + sleepBufferMicroSeconds)
assert(!output.isEmpty && output.removeFirst() == "saved to database")
assert(output.isEmpty)

print("done")

Finally, here are the solutions as operators on Publisher:

extension Publisher where Output == Never {
  func asVoid() -> AnyPublisher<Void, Failure> {
    self
      .map { _ in () }
      .append(Just(()).setFailureType(to: Failure.self))
      .eraseToAnyPublisher()
  }
}

extension Publisher where Output == Void {
  func asNever(completeImmediately: Bool) -> AnyPublisher<Never, Failure> {
    if completeImmediately {
      return Just<Void>(())
        .setFailureType(to: Failure.self)
        .merge(with: self)
        .first()
        .ignoreOutput()
        .eraseToAnyPublisher()
    } else {
      return self
        .ignoreOutput()
        .eraseToAnyPublisher()
    }
  }
}

like image 77
Senseful Avatar answered Sep 30 '22 13:09

Senseful