With async/await in Swift now, getting a value asynchronously is as easy as typing the await keyword. However, sometimes you may not want the value right away. You might want to fire of some asynchronous work to get a value then use it later on, when it is convenient to you. You might event want to cancel it running halfway through and replace the underlying provider, or cancel a value and force it to be refetched.

Task Handles

It turns out we can get most of the way just using Task. If we assign the result of a task to a variable, you've got a "handle" to that task.

let taskHandle: Task<Int, Never> = Task {
    return 100
}

I've written out the type here to make it clear, but this can be inferred by the compiler if your task is not too complex. This is a task that returns an integer and can Never fail.

We now have a way to get the value back out of a task when it's completed! But because we don't know if the task has run to completion yet, we have to still await the value.

let myNumber = await taskHandle.value // async Int

This works well, but I'd like a few additional features that task handles do not provide.

  • A synchronously exposed property to check if the value has already been computed (an Optional that has the value if it's ready). It would be nice to be able to return a value in a synchronous context where I wouldn't be able to with a task handle's .value.
  • The ability to change the async closure from a task. I could decide that a given operation may be too expensive, so I should be able to switch to a less-expensive closure that I have saved for a rainy day.
  • The ability to cancel and recompute a value using the same handle. This might be useful if I've thrown a handle to someone but suddenly decide there's a fresher value they could use.
  • I'm not interested in errors at the moment, so I don't want to have to think about a generic Failure parameter.

Eventual<T>

This is what I'll start with.

/// Asynchrously supply and then remember a result.
/// DON'T COPY AND PASTE - WE'RE NOT DONE
public final class Eventual<T> {
    public var supplier: () async -> T {
        didSet {
            invalidateLastValue()
        }
    }

    public private(set) var value: T?

    public init(supplier: @escaping () async -> T) {
        self.supplier = supplier
    }

    public func get() async -> T {
        if let value = value {
            return value
        } else {
            let computed = await supplier()
            value = computed
            return computed
        }
    }

    public func invalidateLastValue() {
        value = nil
    }
}

Why a class, not an actor? Where's the thread safety!?

Thread Safety

When we store the result of a computation, we want the value to be shared by anyone with a reference to this Eventual<T>. This means we need to use a reference type–a class or actor. Using an actor would mean we lose the ability to synchronously check if the value has been computed yet because access to the actor is mediated from the outside using async/await. Unfortunately this now means this is not thread safe by default, so we'll have to add some kind of locking around the value access. I'm using a small Atomic Primitive I wrote to make this code more readable. As Atomic is a reference type, we hold an immutable reference to it and only modify the internal value via the provided helpers.

private let underlyingValue: Atomic<T?> = .init(initialValue: nil)

public var value: T? {
    underlyingValue.get { $0 }
}

We've now got thread-safe, synchronous access to the value. We don't provide a setter because this should only be updated internally. It would also leave us open to other race conditions for which there's no easy solution in Swift at the moment (see Modify Accessors).

However, we're not free of data races yet. What if while one thread is calling get() another thread changes the supplier? We will need to mediate access to the supplier to ensure there are no race conditions as well.

private let underlyingSupplier: Atomic<() async -> T>
    
public var supplier: () async -> T {
    get {
        underlyingSupplier.get { $0 }
    }
    set {
        underlyingSupplier.modify {
            $0 = newValue
            invalidateLastValue()
        }
    }
}

We change the supplier to a private property and enforce that when it's updated or read from the outside, the lock is held. Also within get(), the closure will not be read until the lock is held.

Sendable

We are now thread safe. This means that we can safely pass the Eventual<T> across threads, but only if T is also safe to send across threads. Swift 5.5 communicates this with the Sendable protocol. So let's say that our Eventual<T> is Sendable if (and only if) T is Sendable.

public final class Eventual<T>: Sendable where T: Sendable {
    ...
}

We can still use Eventual with non-sendable types by wrapping them in an Atomic first.

Unchecked Sendable

When we add a conformance to Sendable, Swift will check the type to make sure all it's members are Sendable. Normally, if a class has any mutable properties, it's not sendable. But because we are mediating access to the mutable state of our Eventual class via an Atomic, this Eventual is also Sendable.

If you know a type is thread safe but the compiler cannot verify it (because it uses locks for example) you can communicate this to the compiler by using @unchecked Sendable. This is actually what the Atomic type does–check out the Gist I've linked at the bottom.

Sendable Closure

While our Eventual is now thread safe, there's no guarantees about what actually runs in the supplying value closure. We would like to ensure that it's not possible for data races to occur, for example, in the following situation.

var number = 0
Eventual<Int> {
    number += 1
    return number
}
Eventual<Int> {
    number += 1
    return number
}

The closures may execute concurrently, just like the closures do in the body of Task or Task.detached. Well, the idea of a type being sendable extends to closures too, meaning they cannot have shared references to mutable state. The Swift compiler will catch situtations like this and give us a compiler error. All we need to do is mark the supplier closure @Sendable.

private let underlyingSupplier: Atomic<@Sendable () async -> T>
    
public var supplier: @Sendable () async -> T {
    get {
        underlyingSupplier.get { $0 }
    }
    set {
        underlyingSupplier.modify {
            $0 = newValue
            invalidateLastValue()
        }
    }
}

Our custom Eventual type is now fully thread safe, neat!

Helpers

I'd also like to be able to create a pre-resolved Eventual, so let's add a helper. This works great as if we only want to synchronously fetch the value, as we will never fire off an async task.

public extension Eventual {
    /// Create a pre-resolved `Eventual<T>`.
    static func value(_ val: T) -> Eventual<T> {
        let eventual = Eventual(supplier: { val })
        eventual.underlyingValue.modify {
            $0 = val
        }
        return eventual
    }
}

It would also be nice if we could directly map from one type to another once the value is resolved.

public extension Eventual {
    func map<U>(body transform: @escaping @Sendable (T) -> U) -> Eventual<U> {
        Eventual<U> {
            let value = await self.get()
            let transformedValue = transform(value)
            return transformedValue
        }
    }
}

Note that this holds a reference to the current Eventual<T> because we don't know what the value is yet, so we need to wait for it to be computed (implicit self capture at self.get(). Therefore, we'll need to be careful not to create a deep stack of maps at the callsite, but it's a good helper for now.

Other Approaches

Having done this exploration, it's always worth considering if it's the best option. Providing and updating values asynchronously sounds awfully similar to Combine's CurrentValueSubject, so it may be worth looking into that if you're in my situation as well. I was looking for of a middle ground between task handles and CurrentValueSubject, so this suits my needs well in the places I'm using it.

For example, a label in a view needs to be set with a user's name (fetched from the network) or set to a fallback placeholder name. I can just pass an Eventual<T> to the view then the view can choose:

  • Synchronously check for a pre-resolved value. If there's no value, just show a placeholder. If it doesn't matter in the current context, I shouldn't be required to run the Task.
  • Asynchronously start a new Task and wait for the value to resolve.

The point is the caller can decide if it's even worth doing the computation in the first place if there's no value already computed.

Complete implementation

Link to full implementation. It's also right here.

/// Asynchrously supply and then remember a result.
public final class Eventual<T>: Sendable where T: Sendable {
    /// Mediated access to the supplying function.
    private let supplierInternal: Atomic<@Sendable () async -> T>

    /// The closure that provides the value.
    ///
    /// This will be called implictly when you `get()` the value.
    public var supplier: @Sendable () async -> T {
        get {
            supplierInternal.get { $0 }
        }
        set {
            supplierInternal.modify {
                $0 = newValue
                invalidateLastValue()
            }
        }
    }

    /// The backing storage for the value, which is thread safe.
    private let underlyingValue: Atomic<T?> = .init(initialValue: nil)

    /// The last computed value by the supplier.
    public var value: T? {
        underlyingValue.get { $0 }
    }

    public init(supplier: @escaping @Sendable () async -> T) {
        self.supplierInternal = .init(initialValue: supplier)
    }

    /// Get the value, either the cached version or compute and then cache.
    public func get() async -> T {
        if let value = value {
            return value
        } else {
            let computed = await supplier()
            underlyingValue.modify {
                $0 = computed
            }
            return computed
        }
    }

    /// Has the value ever been computed as of yet?
    public var isResolved: Bool {
        value != nil
    }

    /// Invalidate the last cached value, forcing a re-computation.
    public func invalidateLastValue() {
        underlyingValue.modify {
            $0 = nil
        }
    }
}

public extension Eventual {
    /// Create a pre-resolved `Eventual<T>`.
    static func value(_ val: T) -> Eventual<T> {
        let eventual = Eventual(supplier: { val })
        eventual.underlyingValue.modify {
            $0 = val
        }
        return eventual
    }

    /// - note: This holds a reference to the previous eventual, due to the nature of the
    /// deferred computation. Therefore, do not create a deep stack of these.
    func map<U>(body transform: @escaping @Sendable (T) -> U) -> Eventual<U> {
        Eventual<U> {
            let value = await self.get()
            let transformedValue = transform(value)
            return transformedValue
        }
    }
}