Home > Enterprise >  Publisher extension not returning unwrapped value
Publisher extension not returning unwrapped value

Time:09-22

I have an extension which takes a publisher and waits until a non-nil value is published before taking its value and returning it for use as an async/await function.

extension Publisher {
  func value() async throws -> Output {
    try await self
      .compactMap { $0 }
      .eraseToAnyPublisher()
      .async()
  }
}

enum AsyncError: Error {
  case finishedWithoutValue
}

extension AnyPublisher {
  /// Returns the first value of the publisher
  @discardableResult
  func async() async throws -> Output {
    try await withCheckedThrowingContinuation { continuation in
      var cancellable: AnyCancellable?
      var finishedWithoutValue = true
      cancellable = first()
        .sink { result in
          switch result {
          case .finished:
            if finishedWithoutValue {
              continuation.resume(throwing: AsyncError.finishedWithoutValue)
            }
          case let .failure(error):
            continuation.resume(throwing: error)
          }
          cancellable?.cancel()
        } receiveValue: { value in
          finishedWithoutValue = false
          continuation.resume(with: .success(value))
        }
    }
  }
}

For some reason, when I use it on an optional @Published value, it returns an optional, rather than returning the unwrapped value of it. Since it waits until a non-nil value returns, why isn't it unwrapping it? For example, assuming foo is an optional published value:

let one = await $foo.value() // Returns an optional
let two = await $foo.compactMap { $0 }.value() // Returns a non-optional

How do I fix this?

CodePudding user response:

The compactMap with function that returns T? will produce a publisher that emits type T, so Publisher<T,Error>. When you call value on it directly the associated type Output is T so the compiler creates a version of value that returns T.

By construction the type of $foo is a publisher whose associated type Output is an optional or T?, Publisher<T?,Error>, so when you call value on that the compiler constructs a function that returns T?, an optional.

Inside of value you use compactMap to strip the optional part away, however, because you've said you want a return Output, which isT?, the compiler implicitly converts your T to a T? and returns it.

You can change your value function so that it specializes for Optionals:

    func value<T>() async throws -> T where Output == Optional<T>{
        try await self
          .compactMap { $0 }
          .eraseToAnyPublisher()
          .async()
    }

The problem is that value will no longer work for non-optional types. You would need two functions... say value and optValue to handle the separate types.

  • Related