まとめ
main.swift
を投げ捨てて、 @main
ディレクティブを持つ別名ファイルを用意する。
経緯
async / await で非同期処理を手続的に記述的に書けるようになったし、ちょっとしたスクリプトも Swift で実装してみるか。。
swift package init --type executable
でパッケージを用意して、 platforms
だけちょっと修正。
platforms: [
.macOS(.v12),
],
あとは、 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"] ?? "")")
}
あーはいはい、 async メソッド呼び出しなので、 Task
で括ってあげる必要があるのね。
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"] ?? "")")
}
}
あれ、結果が表示されない?
Task が非同期実行されるから、Task 完了前に main.swift
の処理が終了しちゃうのか。
Task の実行を待ち合わせるとなると、
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()
いや、これはおかしい。。
対策
Swift のエントリポイントとしては、
main.swift
-
@main
ディレクティブを付与した型のmain()
メソッド
のどちらかを利用できるため、後者を利用します。
@main
ディレクティブを利用する場合、main.swift
が存在してしまうと NG なので、 main.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 もサポートされそうです。