LoginSignup
4
1

More than 1 year has passed since last update.

Swift Concurrency で top-level await できずに困った話

Last updated at Posted at 2021-12-24

まとめ

main.swift を投げ捨てて、 @main ディレクティブを持つ別名ファイルを用意する。

経緯

async / await で非同期処理を手続的に記述的に書けるようになったし、ちょっとしたスクリプトも Swift で実装してみるか。。

swift package init --type executable でパッケージを用意して、 platforms だけちょっと修正。

Package.swift
platforms: [
    .macOS(.v12),
],

あとは、 main.swift に処理を書けば完成っと。

main.swift
import Foundation

let url = URL(string: "https://httpbin.org/delay/3")!

// API を呼び出してデータ取得
let (data, _) = try await URLSession.shared.data(from: url)

// API 呼び出しが完了したら、結果を表示
if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
   let root = jsonObject as? [String: Any],
   let headers = root["headers"] as? [String: Any] {
  print("Your User-Agent is \(headers["User-Agent"] ?? "")")
}

compile_error.png

あーはいはい、 async メソッド呼び出しなので、 Task で括ってあげる必要があるのね。

main.swift
import Foundation

Task {
  let url = URL(string: "https://httpbin.org/delay/3")!

  // API を呼び出してデータ取得
  let (data, _) = try await URLSession.shared.data(from: url)

  // API 呼び出しが完了したら、結果を表示
  if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
     let root = jsonObject as? [String: Any],
     let headers = root["headers"] as? [String: Any] {
    print("Your User-Agent is \(headers["User-Agent"] ?? "")")
  }
}

console.png

あれ、結果が表示されない?
Task が非同期実行されるから、Task 完了前に main.swift の処理が終了しちゃうのか。

Task の実行を待ち合わせるとなると、

main.swift
import Foundation

let semaphore = DispatchSemaphore(value: 0)
Task {
  defer { semaphore.signal() }

  let url = URL(string: "https://httpbin.org/delay/3")!

  // API を呼び出してデータ取得
  let (data, _) = try await URLSession.shared.data(from: url)

  // API 呼び出しが完了したら、結果を表示
  if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
     let root = jsonObject as? [String: Any],
     let headers = root["headers"] as? [String: Any] {
    print("Your User-Agent is \(headers["User-Agent"] ?? "")")
  }
}
semaphore.wait()

console2.png

いや、これはおかしい。。

対策

Swift のエントリポイントとしては、

のどちらかを利用できるため、後者を利用します。

@main ディレクティブを利用する場合、main.swift が存在してしまうと NG なので、 main.swift を適当なファイル名にリネームしつつ、以下のように書き換えます。

Entry.swift
import Foundation

@main
struct Entry {
  static func main() async throws { // ここで定義する main() は async にできる
    let url = URL(string: "https://httpbin.org/delay/3")!

    // API を呼び出してデータ取得
    let (data, _) = try await URLSession.shared.data(from: url)

    // API 呼び出しが完了したら、結果を表示
    if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []),
       let root = jsonObject as? [String: Any],
       let headers = root["headers"] as? [String: Any] {
      print("Your User-Agent is \(headers["User-Agent"] ?? "")")
    }
  }
}

備考

調べてみると、きちんと async / await の Proposal でも言及されていました。

Because only async code can call other async code, this proposal provides no way to initiate asynchronous code. This is intentional: all asynchronous code runs within the context of a "task", a notion which is defined in the Structured Concurrency proposal. That proposal provides the ability to define asynchronous entry points to the program via @main, e.g.,

@main
struct MyProgram {
  static func main() async { ... }
}

Additionally, top-level code is not considered an asynchronous context in this proposal, so the following program is ill-formed:

func f() async -> String { "hello, asynchronously" }

print(await f()) // error: cannot call asynchronous function in top-level code

This, too, will be addressed in a subsequent proposal that properly accounts for top-level variables.

とのことなので、 将来的には main.swift での top-level await もサポートされそうです。

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1