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

## まとめ

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

## 経緯

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

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

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

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

```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"] ?? "")")
}
```

<img width="672" alt="compile_error.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/f1d41186-0886-3dbf-5362-03302a117c37.png">

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

```swift: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"] ?? "")")
  }
}
```

<img width="431" alt="console.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/620d22ae-ba5a-02c7-4344-863e6a78c182.png">

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

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

```swift: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()
```

<img width="431" alt="console2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/178830/1cb1ba4b-68fd-3f18-d5c3-40714d1466f7.png">

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

## 対策

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

- `main.swift`
- `@main` ディレクティブを付与した型の `main()` メソッド
  - https://github.com/apple/swift-evolution/blob/master/proposals/0281-main-attribute.md

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

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

```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 でも言及されていました。

https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md#launching-async-tasks

> 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.,
> 
> ```swift
> @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:
> 
> ```swift
> 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 もサポートされそうです。