Why does Future type need to call attempToFulfill immediately in Combine?

I'm wondering why Future doesn't defer attempToFulfill to reduce the cost when initializing.

In the following example, the console will print "Hi" even there is no subscriber is sinking values.

import Combine

let future = Future<Int, Never> { _ in print("Hi") }

I think Future type should start resolving a result for subscribers only when they request(_ demand:) and the requested demand is greater than zero. Then we don't need to pay the cost for generating a future object.

This is a standard behaviour of most popular future libraries, e.g. BrightFutures and PromiseKit. The benefit is that you don't have to manually retain the future for it to execute, it will either execute immediately or will be retained by a queue it's currently scheduled on. And I do quite like the fact that Combine follows this behavior, this way it's so much easier to migrate from any of those libraries to Combine/OpenCombine.

Besides, you can always wrap your Future with Deferred if needed.

2 Likes

Cool, now I got it.

let defferred = Deferred {
    
    Future<Int, Never> { _ in print("Hi") }
    
}
2 Likes

I also find the default future implementation a bit irritating. I need to port code that creates a future in the objects init and rejects/resolves it at a later point when the operation completes.

I don't have anything to execute in my init, so I can't do anything useful in the Future.init closure, which is why I have to store the provided promise closure in the callers context:

var promise: ((...) -> Void)!
let future = Future<Int, Never> { promise = $0 }

This doesn't exactly inspire confidence and I don't know if this would even be considered intended use.

The Future.Promise closure that you're handed, is according to the docs:

A type that represents a closure to invoke in the future, when an element or error is available.

It is perfectly fine to store this off and save it for future reference. In fact, it is its intended use. However, it is a programmer error to call it multiple times, and I would consider it a code smell to allow the closure to escape a pretty tightly controlled scope. It should be difficult to call it multiple times by mistake.

The best way to achieve that is to simply inline your async code in the Promise initializer and only implicitly store the promise closure by allowing your completion handlers to capture it.

Some promise libs have a factory method that return a (Future, Promise) tuple. Sometimes called differently such as (Promise, Seal) or (Promise, Resolver), (Future, Guarantee) or whatever. But the pattern is the same:

extension Future {
  static func promise() -> (Future, Promise) {
    var promise: Promise!
    let future = Future { promise = $0 }
    assert(promise != nil)
    return (future, promise)
  }
}

But I'm not sure it gives you much.

Where did you read that?

It is not completely impossible in my case that the promise could be resolved multiple times. I made tests and it seems that the first resolved result is the only one that is used (as it should be).