7

I have a method that returns a Future:

func getItem(id: String) -> Future<MediaItem, Error> {
  return Future { promise in
    // alamofire async operation
  }
}

I want to use it in another method and covert MediaItem to NSImage, which is a synchronous operation. I was hoping to simply do a map or flatMap on the original Future but it creates a long Publisher that I cannot erased to Future<NSImage, Error>.

func getImage(id: String) -> Future<NSImage, Error> {
  return getItem(id).map { mediaItem in
    // some sync operation to convert mediaItem to NSImage
    return convertToNSImage(mediaItem)  // this returns NSImage
  }
}

I get the following error:

Cannot convert return expression of type 'Publishers.Map<Future<MediaItem, Error>, NSImage>' to return type 'Future<NSImage, Error>'

I tried using flatMap but with a similar error. I can eraseToAnyPublisher but I think that hides the fact that getImage(id: String returns a Future.

I suppose I can wrap the body of getImage in a future but that doesn't seem as clean as chaining and mapping. Any suggestions would be welcome.

Kon
  • 4,023
  • 4
  • 24
  • 38

2 Answers2

5

You can't use dribs and drabs and bits and pieces from the Combine framework like that. You have to make a pipeline — a publisher, some operators, and a subscriber (which you store so that the pipeline will have a chance to run).

 Publisher
     |
     V
 Operator
     |
     V
 Operator
     |
     V
 Subscriber (and store it)

So, here, getItem is a function that produces your Publisher, a Future. So you can say

getItem (...)
    .map {...}
    ( maybe other operators )
    .sink {...} (or .assign(...))
    .store (...)

Now the future (and the whole pipeline) will run asynchronously and the result will pop out the end of the pipeline and you can do something with it.

Now, of course you can put the Future and the Map together and then stop, vending them so someone else can attach other operators and a subscriber to them. You have now assembled the start of a pipeline and no more. But then its type is not going to be Future; it will be an AnyPublisher<NSImage,Error>. And there's nothing wrong with that!

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • In my case, `getItem()` is a private function, and I'd like to expose `getImage()` to the user, who will then create the subscription. I'd like to create the pipeline up to the point of `sink` as part of `getImage` and expose a simple Future to the user. With a publisher, I can do that and `eraseToAnyPublisher` to hide the details. – Kon Mar 16 '20 at 00:49
  • Certainly, you can produce any part of the pipeline you like. But once you have said `.map`, you do not have a "simple Future". You have a pipeline. You can erase it to AnyPublisher but now its type is whatever type the `map` produces. There's no point saying you can't erase it to Future; that's right, it's _not_ a future. – matt Mar 16 '20 at 00:56
  • In fact, the error you quoted shows exactly what it is: it's a Future-followed-by-a-Map (that is what `Map – matt Mar 16 '20 at 01:07
  • 2
    With a Future, I want to convey to the user that the function will only return a single value and complete. – Kon Mar 16 '20 at 01:32
  • Why do you need to convey that? The pipeline itself will convey that by doing it. You can _document_ that, but you cannot typologize the pipeline in such a way as to convey it. – matt Mar 16 '20 at 01:42
  • Of course, you _could_ turn your MediaItem into an NSImage as _part_ of the Future, _before_ calling `promise`. – matt Mar 16 '20 at 01:55
  • Understood. Thanks for the clarifications. – Kon Mar 16 '20 at 02:20
2

You can always wrap one future in another. Rather than mapping it as a Publisher, subscribe to its result in the future you want to return.

func mapping(futureToWrap: Future<MediaItem, Error>) -> Future<NSImage, Error> {
    var cancellable: AnyCancellable?
    return Future<String, Error> { promise in
        // cancellable is captured to assure the completion of the wrapped future
        cancellable = futureToWrap
            .sink { completion in
                if case .failure(let error) = completion {
                    promise(.failure(error))
                }
            } receiveValue: { value in
                promise(.success(convertToNSImage(mediaItem)))
            }
    }
}

This could always be generalized to

extension Publisher {
    func asFuture() -> Future<Output, Failure> {
        var cancellable: AnyCancellable?
        return Future<Output, Failure> { promise in
            // cancellable is captured to assure the completion of the wrapped future
            cancellable = self.sink { completion in
                    if case .failure(let error) = completion {
                        promise(.failure(error))
                    }
                } receiveValue: { value in
                    promise(.success(value))
                }
        }
    }
}

Note above that if the publisher in question is a class, it will get retained for the lifespan of the closure in the Future returned. Also, as a future, you will only ever get the first value published, after which the future will complete.

Finally, simply erasing to AnyPublisher is just fine. If you want to assure you only get the first value (similar to getting a future's only value), you could just do the following:

getItem(id)
    .map(convertToNSImage)
    .eraseToAnyPublisher()
    .first()

The resulting type, Publishers.First<AnyPublisher<Output, Failure>> is expressive enough to convey that only a single result will ever be received, similar to a Future. You could even define a typealias to that end (though it's probably overkill at that point):

typealias AnyFirst<Output, Failure> = Publishers.First<AnyPublisher<Output, Failure>>
kid_x
  • 1,415
  • 1
  • 11
  • 31