This makes sense. I assume then in the case that an ObjC method fails to call its completion handler that the Swift call would never return. What exactly is the behavior then if a translated ObjC method calls its completion handler more than once?
This is described in https://github.com/DougGregor/swift-evolution/blob/concurrency-objc/proposals/NNNN-concurrency-objc.md#completion-handlers-must-be-called-exactly-once:
Fortunately, because the compiler itself is synthesizing the block that will be passed to completion-handler APIs, it can detect both problems by introducing an extra bit of state into the synthesized block to indicate that the block has been called. If the bit is already set when the block is called, then it has been called multiple times. If the bit is not set when the block is destroyed, it has not been called at all. While this does not fix the underlying problem, it can at least detect the issue consistently at run time.
Are you asking what the specific failure mode will be?
Doug
Thank you for the link. I was mainly worried that some strange undefined behavior would be introduced in such cases. I would expect the program to crash when the issue is detected. Is that the case?
Either that or have the runtime produce a warning that the task is stuck and cannot be completed. It's easy to do either once we've detected the condition.
Doug
Looking at [Concurrency] Structured concurrency, I guess the Objective-C -> Swift import could work by having the progress being imported as a property of the underlying Task
.
For instance, if we had the method described before, you could do:
async let result = try doSoethingThatTakesALongTime()
let progress = $result.progress // access the underlying `Task` and extract the progress from it
For the case of the @objc
method implemented in swift, the progress could be generated by the system and made accessible, for instance using a single on Task
:
func doSoethingThatTakesALongTime() throws async -> MyResult
{
let progress = Task.current.progress // get a progress that the method can update, attach children, ...
}
or by passing it as a parameter of the method:
func doSoethingThatTakesALongTime(progress: Progress) throws async -> MyResult
{
}
What do you think?
It's plausible, with enough coupling between Task
and Progress
. This are has not yet been explored in depth.
Doug
What are the ideas on existing API where the asynchronous continuation is not via a completion handler function but through a delegate method?
Here is an example of what I mean:
let p: CBPeripheral = ...
p.discoverServices()
Now control flow continues in the delegate:
class MyDelegate: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral,
didDiscoverServices error: Error?) {
...
}
}
...
}
It would be so much nicer to have discoverServices
as an async method of course.
Yeah. This is the goal behind the not-properly-pitched @asyncHandler
, where you will be able to write:
class MyDelegate: CBPeripheralDelegate {
@asyncHandler
func peripheral(_ peripheral: CBPeripheral,
didDiscoverServices error: Error?) {
...
}
}
...
}
And this synchronously-called function's body will behave "as if" wrapped in a call to Task.runDetached
making the body effectively async
.
Doug
The right approach for APIs with more structure to their async-ness than just a completion handler is to look at it carefully, decide what that extra structure is trying to achieve, and then think about how to capture it in a more lexically-structured way so that ultimately the whole thing can be an async operation. In many cases there's an "operation" object that exists pretty much solely to provide a handle for cancellation, which can be made into an implementation detail of the API by setting up a Swift cancellation handler that triggers it. (We haven't pitched the cancellation-handler API yet.) In more complex cases where there's a lot of back-and-forth with the API, I think it really just needs to be restructured as an async API where the intermediate back-and-forth is done with a delegate (or whatever) but the final results are delivered normally as the return value.
Ok, this asyncHandler allows me to use await
in delegate Methods - that’s nice but I think John answered what I had in mind - i.e. be able to call the method like this:
await try peripheral.discoverServices()
// continue control flow here and not in some delegate handler
I’m confused by this because the async/await spec actually calls this out as explicitly disallowed:
These statements seem to be in conflict.
IIUC the current state of the proposals is such that directly defining functions overloaded on async
-ness is illegal, but Objective-C import rules are permitting to create Swift entry points which are effectively overloads on async
-ness.
Yes, that's correct. This also isn't new: The Objective-C translation into Swift has always bent some redeclaration rules, leaving it to overload resolution to sort out the expected behavior. In pure Swift, you can also end up with async and synchronous overloading if the two declarations come from different modules.
I'll need to update this interoperability proposal's wording to account for the change to the async/await proposal, though, so this isn't confusing.
Doug
Large organizations running decades old business NEED to bridge objc legacy to Swift. For pure "great Swift experience" you must sacrifice pretty much of man-hour.