【Swift】async letの基本的な使い方

## async letが導入された背景
async letが導入される前、非同期処理を並列で実行するには`TaskGroup`を使う必要がありました。
```Swift
let result = await withTaskGroup(of: Int.self) { group -> Int in
        // 複数のタスクを追加
        group.addTask { await fetchNumber(1) }
        group.addTask { await fetchNumber(2) }
        group.addTask { await fetchNumber(3) }

        // ...
}
```

しかしTaskGroupにはいくつかの問題点がありました。
- 異なる型の結果を扱う際コードが冗長になる
- **ただ子タスクが親タスクに単一の値を返せばいい**だけの場合でも、冗長なボイラーコードが必要

その問題を解決するため、簡単に非同期の並列処理を行える`async let`が導入されました。

## async letの使い方
`async let`は宣言の左部分に付属させるだけで使用できます。
```Swift
async let value = fetchData()
```
この式で宣言された時点で、子タスクとして`fetchData`が非同期で処理されるようになります。

この`value`の値を使いたい時は、`await`を使用する必要があります。逆に`await`を使うまでは、処理は中断されません。
```Swift
print(await value)
```

もし`throws`がついた関数を並列で処理したい場合は、結果を取得する際に`try`をつける必要があります。
```Swift
async let result = fetchData() // fetchDataはthrowsがついた関数
print(try await result)
```

### TaskGroupが抱えていた問題を解決できたのか？
先述した通りTaskGroupには、**異なる型の結果を扱う際コードが冗長になる**という問題点がありました。

例えば以下のコードのような場合です。
```Swift
// 異なる型の結果を扱うための列挙型
enum FetchResult {
    case number(Int)
    case text(String)
}

func fetchNumber() async throws -> Int {
    try await Task.sleep(for: .seconds(1))
    return 42
}

func fetchText() async throws -> String {
    try await Task.sleep(for: .seconds(1))
    return "Hello, World!"
}

// TaskGroup を使用して異なる型の結果を取得
func fetchAllWithTaskGroup() async throws -> (Int, String) {
    var number: Int?
    var text: String?
    
    try await withThrowingTaskGroup(of: FetchResult.self) { group in
        group.addTask {
            return .number(try await fetchNumber())
        }
        group.addTask {
            return .text(try await fetchText())
        }
        
        for try await result in group {
            switch result {
            case .number(let fetchedNumber):
                number = fetchedNumber
            case .text(let fetchedText):
                text = fetchedText
            }
        }
    }
    
    return (number!, text!)
}

Task {
    do {
        let (number, text) = try await fetchAllWithTaskGroup()
        print("Number: \(number)")
        print("Text: \(text)")
    } catch {
        print("エラーが発生しました: \(error)")
    }
}
```

Stringが返り値の`fetchText`と、Intが返り値の`fetchNumber`を並列で処理するために、`FetchResult`を定義しています。ぱっと見かなり複雑で読みづらいコードだと思います。

こちらのコードを`async let`を使って描き直したのがこちらです。

```Swift
func fetchNumber() async throws -> Int {
    try await Task.sleep(for: .seconds(1))
    return 42
}

func fetchText() async throws -> String {
    try await Task.sleep(for: .seconds(1))
    return "Hello, World!"
}

// async let を使用して異なる型の結果を取得
func fetchAllWithAsyncLet() async throws -> (Int, String) {
    // 並行してタスクを開始
    async let number = fetchNumber()
    async let text = fetchText()
    
    // 結果を待機して取得
    let finalNumber = try await number
    let finalText = try await text
    
    return (finalNumber, finalText)
}

// 使用例
Task {
    do {
        let (number, text) = try await fetchAllWithAsyncLet()
        print("Number: \(number)")
        print("Text: \(text)")
    } catch {
        print("エラーが発生しました: \(error)")
    }
}
```
かなりシンプルで読みやすくなったと思います。

- 並列処理したい関数の戻り値が異なる型の場合
- 並列で処理したいタスクの個数が決まっている場合

これらの場合は、`async let`を使って記述するとシンプルに書けるようになると思います。


## `async let`で生成されたタスクのキャンセルについて
`async let`で生成されるタスクは呼び出し元の親タスクの子タスクとして生成されます。
そのため親タスクがキャンセルされると、`async let`の処理もキャンセルされることになります。

以下は並列処理中に親タスクがキャンセルされる例です。

```Swift
func longRunningTask(label: String) async -> Int {
    do {
        for i in 1...5 {
            try Task.checkCancellation() // キャンセルをチェック
            print("\(label): Running step \(i)")
            try await Task.sleep(for: .seconds(1)) // 1秒待機
        }
        print("\(label): Completed")
        return 42
    } catch {
        print("\(label): Cancelled")
        return -1
    }
}

func main() async {
    async let taskA = longRunningTask(label: "Task A")
    async let taskB = longRunningTask(label: "Task B")

    print("Parent task: Starting child tasks...")

    let values = await(taskA, taskB)
    print(values)
}

let parent = Task {
    await main()
}

// 親タスクを2.5秒後にキャンセル
Task {
    try? await Task.sleep(for: .seconds(2.5)) // 2.5秒待機
    parent.cancel()
    print("Parent task: Cancelled")
}
```

**実行結果(順番は前後する可能性がある)**
```text
Parent task: Starting child tasks...
Task A: Running step 1
Task B: Running step 1
Task A: Running step 2
Task B: Running step 2
Task B: Running step 3
Task A: Running step 3
Task B: Cancelled
Parent task: Cancelled
Task A: Cancelled
(-1, -1)
```

実行結果を見るとわかる通り、親タスクがキャンセルされると、`async let`で定義した処理もキャンセルされています。

## 参考にした記事
- https://github.com/swiftlang/swift-evolution/blob/main/proposals/0317-async-let.md
- https://zenn.dev/yuki216/articles/ee1bd18907278c