@TaskLocal触ってみる

`@TaskLocal`プロパティラッパーを触る機会があったので、使い方の備忘録として記事にしました。

TaskLocal は static なグローバル変数を、コンテキストを限定して値を注入して、注入した値を読み取ることができる様にします。
https://developer.apple.com/documentation/swift/tasklocal

TaskLocalは以下OSバージョンから使うことができます。（結構昔から使えてたみたいで、驚きました）
- iOS 13.0以降
- iPadOS 13.0以降
- Mac Catalyst 13.0以降
- macOS 10.15以降
- tvOS 13.0以降
- visionOS 1.0以降
- watchOS 6.0以降

:::note
Swift6.0 からは、static なグローバル変数だけでなく、static でないグローバル変数を TaskLocal を適用できるようです。
https://developer.apple.com/documentation/swift/tasklocal#Task-local-declarations
:::

公式のドキュメントでは以下の様に説明されています。

> A task-local value is a value that can be bound and read in the context of a Task. It is implicitly carried with the task, and is accessible by any child tasks it creates (such as TaskGroup or async let created tasks).

日本語訳

> タスクローカル値とは、 のコンテキスト内でバインドおよび読み取り可能な値です Task。この値はタスクに暗黙的に保持され、タスクによって作成されたすべての子タスク（TaskGroup や作成されたタスクなど async let）からアクセスできます。

文面で説明を見ても、イメージ掴みづらい気がするので、どんな動作をするのか実際のコードから確認していきます。

## サンプルコードで動作確認
例のコードとして以下を用意しました。

```swift
struct Store {
    @MainActor static var value: Int = 0
}

@MainActor
func main() {
    print("value: \(Store.value)")

    Store.value = 1

    print("value: \(Store.value)")
}

main()
```

`main()`の実行で、コンソールに以下が出力されます。

```console
value: 0
value: 1
```

最初は`Store.value`には 0 が入っているから`value: 0`と出力され、
そのあと`Store.value`に`1`を代入しているので、`value: 1`が出力されます。

これは、`Store.value`の値の変更がグローバルなコンテキストで行われているからです。

この例に`@TaskLocal`を用いて、`Store.value`の値の変更のコンテキストを限定してみましょう。

```swift
struct Store {
    @TaskLocal static var value: Int = 0
}

@MainActor
func main() {
    print("value on global context: \(Store.value)")

    Store.$value.withValue(100) {
        print("value on context1: \(Store.value)")
    }

    print("value on global context: \(Store.value)")
}

main()
```

`main()`の実行で、コンソールに以下が出力されました。

```console
value on global context: 0
value on context1: 100
value on global context: 0
```

`Store.value`に`@TaskLocal`を付与すると、`Store.$value`の様に、`$value`に参照できます。
`$value`は`TaskLocal<Int>`型で、[withValue](<https://developer.apple.com/documentation/swift/tasklocal/withvalue(_:operation:file:line:)>)メソッドを使って、限定したコンテキストからのみ読み取りできる値を注入できます。

今回だと、`Store.$value.withValue(100)`とすることで、クロージャ内のコンテキストから`Store.value`を参照したときに`100`が取れるようになります。
そのためコンソールの主力結果は、クロージャ内のログは`value on context1: 100`となり、それ以外のログは、`value on global context: 0`となりました。

ここまでで、TaskLocal を使うことで、コンテキストを限定した値の注入のイメージが少し沸いたでしょうか。

:::note
ちなみにの話ですが、Swift6 環境では、static な変数は通常複数のスレッドから読み書き可能な状態になってしまうため、Actor に隔離するなどしてスレッドセーフな状態にしないとコンパイルエラーが出てしまいます。
ただ、`@TaskLocal`が付与されていれば、表面上は read only の定数として振る舞う様になります。実際に値を変更しようとすると、以下の様なエラーが表示されるでしょう。

```
Cannot assign to property: 'value' is a get-only property
```

static な定数であれば、Sendable である限りスレッドセーフであることが保証されるので、`@MainActor`で Actor に隔離せずともコンパイルエラーが表示されなくなるので、`@MainActor`は削除しています。
:::

## withValue で注入した値の生存期間

TaskLocalでコンテキストを限定した値の生存期間はどのくらいか気になったので、もう少し動作を深掘りしました。
[ドキュメント](https://developer.apple.com/documentation/swift/tasklocal#Binding-task-local-values)によると、`withValue`メソッドで注入した値は、`withValue`メソッドの`operation`クロージャ内でのみ有効ですが、クロージャ内であっても`Task.detached`内でも注入した値は引き継がれないようです。
ただ、`Task`のイニシャライザのクロージャ内であれば、注入した値は引き継がれます。

以下サンプルコードで動作を確認してみましょう。

```swift
@MainActor
func main() {
    print("value on global context: \(Store.value)")

    Store.$value.withValue(100) {
        print("value on context1: \(Store.value)")

        Task {
            print("value on context1 in task: \(Store.value)")

            Task {
                print("value on context1 in task2: \(Store.value)")
            }
        }

        Task.detached {
            print("value on context1 in detached task: \(Store.value)")
        }
    }

    print("value on global context: \(Store.value)")
}

main()
```

`main()`の実行結果は以下のとおり。

```console
value on global context: 0
value on context1: 100
value on context1 in detached task: 0
value on global context: 0
value on context1 in task: 100
value on context1 in task2: 100
```

ドキュメントに記載されている通り、`withValue`のクロージャ内で、`Task.detached`のクロージャ内の`print`出力以外では注入した値が取れていそうです。
`Task.detached`のクロージャ内は想定通り、デフォルトの`0`が取れていました。

ただ、ドキュメントでは記載されていない内容で、`DispatchQueue`のクロージャでも`Task.detached`と同じく注入した値は引き継がれませんでした。

```swift
@MainActor
func main() {
    print("value on global context: \(Store.value)")

    Store.$value.withValue(100) {
        print("value on context1: \(Store.value)")

        DispatchQueue.main.async {
            print("value on context1 in DispatchQueue.main: \(Store.value)")
        }

        DispatchQueue.global().async {
            print("value on context1 in DispatchQueue.global(): \(Store.value)")
        }
    }

    print("value on global context: \(Store.value)")
}

main()
```

```console
value on global context: 0
value on context1: 100
value on global context: 0
value on context1 in DispatchQueue.global(): 0
value on context1 in DispatchQueue.main: 0
```

`withValue`メソッドの戻り値として、`operation`クロージャ内で return したインスタンスを返す様になっているのですが、
その特徴から、`withValue`の戻り値の`viewModel`の中でも注入した`Store.value`の読み取りができるのかなと勘違いしていました。
クロージャ外に出ている時点で TaskLocal のコンテキスト外なので、デフォルトの`Store.value`の値しか読み取れません。
以下のコードの場合は、`ViewModel.onAppear`実行時は`value on ViewModel: 0`と表示されます。

```swift
final class ViewModel {
    func onAppear() {
        print("value on ViewModel: \(Store.value)")
    }
}

struct Store {
    @TaskLocal static var value: Int = 0
}

@MainActor
func main() {
    var viewModel = Store.$value.withValue(1) {
        return ViewModel()
    }
    viewModel.onAppear()
}

main()
```

`ViewModel`のインスタンス生成時など、`withValue`の`operation`クロージャ内で`Store.value`の値を保持する様にすれば、TaskLocal のコンテキスト外でも値を読み取りできます。

```swift
final class ViewModel {
    let value: Int

    init() {
        value = Store.value
    }
    func onAppear() {
        print("value on ViewModel: \(value)")
    }
}

struct Store {
    @TaskLocal static var value: Int = 0
}

@MainActor
func main() {
    var viewModel = Store.$value.withValue(1) {
        return ViewModel()
    }
    viewModel.onAppear()
}

main()
```

## 使い所は？

`@TaskLocal`の使い方や挙動を見てきましたが、結局このAPI何に使えるのか考えてみました。
まず最初に思い当たるのは、DIの仕組みとして使用できることです。
どうやってDIの仕組みとして使えるかは、以下記事を参照ください。

https://zenn.dev/prevent/articles/a874e3660b08d2#tasklocal%E3%81%AB%E3%82%88%E3%82%8Bdi

また、以下記事では、Loggerのログレベルを制御するのに、`@TaskLocal`を用いる例もありました。

https://www.hackingwithswift.com/quick-start/concurrency/how-to-create-and-use-task-local-values