Skip to content

[5.5][Async Refactoring] Wrap code in a continuation if conversion doesn't yield reasonable results #38256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 7, 2021

Conversation

ahoppen
Copy link
Member

@ahoppen ahoppen commented Jul 5, 2021

  • Explanation: If we are requested to convert a function to async, but the call in the function’s body that eventually calls the completion handler doesn’t have an async alternative, we are currently copying the call as-is, replacing any calls to the completion handler by placeholders. This is seldom what the developer wants and it would be better to wrap the function in withChecked(Throwing)Continuation and replace all calls to the completion handlers by calls to continuation.resume(throwing/returning:).
  • Scope: Async refactoring where the completion handler is called from a function that doesn’t have an async alternative
  • Risk: Low
  • Testing: Added regression test
  • Issue: rdar://79304583
  • Reviewer: @bnbarham (Ben Barham), @hamishknight (Hamish Knight), @ktoso (Konrad Malawski) on original PR [Async Refactoring] Wrap code in a continuation if conversion doesn't yield reasonable results #38193

Example 1

Base Code

func testDispatch(completionHandler: @escaping (Int) -> Void) {
  DispatchQueue.global.async {
     completionHandler(longSyncFunc())
  }
}

Old Refactoring Result

func testDispatch() async -> Int  {
  DispatchQueue.global.async {
     <#completionHandler#>(longSyncFunc())
  }
}

New Refactoring Result

func testDispatch() async -> Int {
  return await withCheckedContinuation { continuation in
    DispatchQueue.global.async {
      continuation.resume(returning: longSyncFunc())
    }
  }
}

Example 2

Base Code

func testUrlSession(completionHandler: @escaping (Data) -> Void) {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    completion(data!)
  }
  task.resume()
}

Old Refactoring Result

func testUrlSession() async -> Data {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    <#completion#>(data!)
  }
  task.resume()
}

New Refactoring Result

func testDataTask() async -> Int?
  return await withCheckedContinuation { continuation in
    let task = URLSession.shared.dataTask { data, response, error in
      continuation.resume(returning: data!)
    }
    task.resume()
  }
}

@ahoppen ahoppen added the r5.5 label Jul 5, 2021
@ahoppen ahoppen requested a review from akyrtzi July 5, 2021 08:55
@ahoppen ahoppen requested a review from a team as a code owner July 5, 2021 08:55
@ahoppen
Copy link
Member Author

ahoppen commented Jul 5, 2021

@swift-ci Please test

… yield reasonable results

If we are requested to convert a function to async, but the call in the function’s body that eventually calls the completion handler doesn’t have an async alternative, we are currently copying the call as-is, replacing any calls to the completion handler by placeholders.

For example,
```swift
func testDispatch(completionHandler: @escaping (Int) -> Void) {
  DispatchQueue.global.async {
     completionHandler(longSyncFunc())
  }
}
```
becomes
```swift
func testDispatch() async -> Int  {
  DispatchQueue.global.async {
     <#completionHandler#>(longSyncFunc())
  }
}
```

and

```swift
func testUrlSession(completionHandler: @escaping (Data) -> Void) {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    completion(data!)
  }
  task.resume()
}
```
becomes
```swift
func testUrlSession() async -> Data {
  let task = URLSession.shared.dataTask(with: request) { data, response, error in
    <#completion#>(data!)
  }
  task.resume()
}
```

Both of these are better modelled using continuations. Thus, if we find an expression that contains a call to the completion handler and can’t be hoisted to an await statement, we are wrapping the rest of the current scope in a `withChecked(Throwing)Continuation`, producing the following results:

```swift
func testDispatch() async -> Int {
  return await withCheckedContinuation { (continuation: CheckedContinuation<Int, Never>) in
    DispatchQueue.global.async {
      continuation.resume(returning: syncComputation())
    }
  }
}
```

and

```swift
func testDataTask() async -> Int?
  return await withCheckedContinuation { (continuation: CheckedContinuation<Data, Never>) in
    let task = URLSession.shared.dataTask { data, response, error in
      continuation.resume(returning: data!)
    }
    task.resume()
  }
}
```

I think both are much closer to what the developer is actually expecting.

Resolves rdar://79304583

# Conflicts:
#	lib/IDE/Refactoring.cpp
@ahoppen ahoppen force-pushed the pr-5.5/use-continuation branch from 56edd5e to a4f3f3b Compare July 5, 2021 13:48
@ahoppen
Copy link
Member Author

ahoppen commented Jul 5, 2021

@swift-ci Please test

@akyrtzi akyrtzi merged commit 6a883c7 into swiftlang:release/5.5 Jul 7, 2021
@ahoppen ahoppen deleted the pr-5.5/use-continuation branch July 8, 2021 07:55
@AnthonyLatsis AnthonyLatsis added 🍒 release cherry pick Flag: Release branch cherry picks swift 5.5 labels Jan 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🍒 release cherry pick Flag: Release branch cherry picks swift 5.5
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants