~Copyable and Completion Handlers

I'd like to write some code where I can be statically assured that a completion handler is going to be invoked. Something like this:

struct CompletionHandler<T>: ~Copyable {
    private let f: (T) -> Void
    init(f: consuming @escaping (T) -> Void) { self.f = f }
    consuming func callAsFunction(_ t: T) -> Void {
        f(t)
    }
}

Basically the idea is that cleanup here requires the invocation of the handler bc the handler's closure has captured resources that it needs to release or otherwise resolve.

What I would hope is that passing an instance of this type to another function like this:

func invokeHandler<A>(_ completion: consuming CompletionHandler<A>) {
    // don't invoke the handler
}

would fail to compile because the only consuming path through the type has not been invoked.

But this code seems fine, I'm assuming bc the consume can simply deinit the completion handler. Is what I'm trying to do even possible or perhaps planned but unimplemented?

1 Like

As a follow-up, I'm not sure what this code means:

func invokeHandler<A>(_ completion: consuming @escaping (A) -> Void) {
    // don't invoke the handler
}

if the completion handler is not invoked. Is consuming there just a no-op?

All types today are implicitly destructed when they aren't consumed, so there isn't a way yet to statically enforce that it is always explicitly consumed. You could dynamically check it by having the deinit raise a fatalError, maybe.

3 Likes

I thought that might be the case, and that is where I've ended up, but that sorta puts discovery of this error off to a unit test where it would be nice to have it static. Didn't see it in SE-390, but it would certainly be a lovely future direction from my point of view.

It gets tricky because of things like early exits and generics. One way to think of it is that ~Ignorable is a similar condition to ~Copyable, but a distinct one. The technical term for this is “linear types” or “relevant types”, and you can find some previous discussion on this forum. I think this one’s probably the most complete recent thread on it:

3 Likes

Thanks @jrose, I had forgotten about that previous thread. after thinking about it over night I'm really kind of interested in my second question above. What does the consuming in:

func invokeHandler<A>(_ completion: consuming @escaping (A) -> Void) {
    // don't invoke the handler
}

actually do? That compiles just fine but requires the @escaping so I assume that it expects completion to be on the heap, but I'm not quite sure of the lifetime implications there, i.e. I can't figure out anything that would happen in the body of invokeHandler with the consuming decorator that would not also happen without it.

My confusion here was that intuitively, the way to consume an instance of a function type would entail invoking it, but that's clearly not the case.

Interestingly if I try to do this in a curried function, the parser seems to have some trouble with it. Specifically this compiles:

public func invokeCurriedHandler1<A>(a: consuming A) -> (consuming @escaping (A) -> Void) -> Void {
    { a in
    }
}

but this fails:

func invokeCurriedHandler2<A>(a: consuming A) -> (consuming @escaping (A) -> Void) -> Void {
    { (a: consuming A) -> (consuming @escaping (A) -> Void) -> Void in
    }
}

with:

Cannot find 'consuming' in scope

on both uses of the consuming keyword.

@escaping closures are copyable types, and borrowing/consuming on copyable types doesn't really have a guaranteed static effect on the value's lifetime, since you can always extend the lifetime by just copying the value. Using consuming is an optimization when the caller is likely to have the only remaining reference to a value, allowing the caller to store the argument or use it as part of an aggregate return value without making a copy of it, or just end its lifetime earlier.

3 Likes

Can you please tell more specifically what does it mean "copyable types" in this context? (keeping in mind that closures have ref semantics) Is it relevant for only @escaping closures or not?

Types are copyable if identical copies can be made of them (more formally, they satisfy the Copyable constraint, which all Swift types currently do unless they are declared ~Copyable). For so-called reference types, the reference is the value which gets copied.

1 Like

One trick you can use to achieve that completion is called, is to use generic type for result so that the only way to construct result of the function is to call completion handler:

struct CompletionHandler<T, R>: ~Copyable {
    private let f: (T) -> R
    init(f: consuming @escaping (T) -> R) { self.f = f }
    consuming func callAsFunction(_ t: T) -> R {
        f(t)
    }
}

func invokeHandler<A, R>(_ completion: consuming CompletionHandler<A, R>) -> R {
    // here we know nothing about R
    // the only way to construct return value is to call completion()
}

1 Like

ah... so we make it a full on continuation monad. I LIKE that. wonder if i can add a similar dummy type to ensure that its only invoked once and get a roll-my-own linear type system.