[Swift] actor の isolated / nonisolated キーワードの使い方

## 概要
actor のメソッドやプロパティは actor に isolate されています。 [SE-0313 Improved control over actor isolation](https://github.com/apple/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md) のプロポーザルで追加された `isolated` / `nonisolated` キーワードを使うことで、 actor のメンバーでない処理を actor に isolate したり、逆に actor のメンバーを actor に isolate しないことができます。この記事では、プロポーザルをもとにこれらのキーワードの使い方をまとめます。

記事中の動作検証は Xcode 14 Beta 5 で行っています。

## `isolated` キーワード

actor の例として、以下の `Counter` 考えます。

```swift
actor Counter {
    var count: Int = 0
}
```

この actor の `count` を外部からインクリメントしたいとします。 actor に isolate された状態はデータ競合から保護するために外部から直接操作できないようになっているため、以下のような実装はエラーになります。

```swift
func incrementCounter(_ counter: Counter) {
    counter.count += 1 // ❗️ Actor-isolated property 'count' can not be mutated from a non-isolated context
}
```

ここで、パラメータに対して `isolated` キーワードをつけることで、 `incrementCounter` 関数を `counter` actor に isolate することができます。これにより、同期的に actor のプロパティである `count` を操作できるようになります。

```swift
func incrementCounter(_ counter: isolated Counter) {
    counter.count += 1 // ✅ OK
}
```

この `incrementCounter` は `async` 関数ではありませんが、 actor に isolate されているために actor 外から呼ぶ際には `await` する必要があります。actor に isolate されている以上 actor のメソッドと同じ性質を持つというわけです。

```swift
@main
public struct Main {
    public static func main() async throws {
        let counter = Counter()
        await incrementCounter(counter)
    }
}
```

### `isolated` 関連のエラー

`isolated` キーワードは actor にしかつけられないようになっています。関数が `isolated` がつけられたパラメータに isolate されるという性質上、 actor でないパラメータに `isolated` をつけるのは意味をなさないためです。

```swift
// ❗️ 'isolated' parameter has non-actor type 'Int'
func incrementCounter(_ counter: Counter, by value: isolated Int) {}
```

また、 `isolated` を１つの関数の中で複数のパラメータにつけることはできません。これは関数を複数の actor に isolate することができないためです。

```swift
// ❗️エラー
func incrementCounters(_ counter1: isolated Counter, _ counter2: isolated Counter) {}
```

同様の理由で、 actor のメソッドに `isolated` キーワードをつけることもできません。

```swift
extension Counter {
    // ❗️エラー
    func transferCount(to other: isolated Counter) {}
}
```

ただし、 Xcode 14 Beta 5 の時点ではいずれのコードもコンパイルエラーや警告なしで実行することができ、試してみると実際にデータ競合を発生させることもできてしまいます。この問題はそのうち修正され、上記のコードは実行できなくなるはずです。

ref: https://github.com/apple/swift/issues/60474

### `isolated` の使いどころ

`isolated` キーワードですが、絶対に使わなければいけない場面というのは思いつきません（もしご存知の方がいればコメントで教えてください）。 actor に isolate された処理がほしければ actor のメソッドにしてしまえばいいためです。
ただし、コード管理やコードの書き味の観点で actor のメソッドよりもグローバルな関数にして `isolated` キーワードを使う方が適切な場面というのはありそうなので、覚えておいて損はないでしょう。

## `nonisolated` キーワード

`isolated` キーワードを使うことで actor 外の関数を actor に isolate することができることがわかりました。 `nonisolated` キーワードを使うことで、逆に actor のメンバーを isolate しないことができます。
例えば、以下のように actor のメソッドに `nonisolated` をつけると、 isolate されなくなるため actor 外から同期的に呼べるようになります。

```swift
extension Counter {
    func f1() {}
    nonisolated func f2() {}
}

@main
public struct Main {
    public static func main() async throws {
        let counter = Counter()
        counter.f1() // ❗️ Expression is 'async' but is not marked with 'await'
        counter.f2() // ✅ OK
    }
}
```

`nonisolated` キーワードは `var` プロパティにつけるとエラーになります。そもそも actor は可変な状態を守るためのものなので、 `var` を isolate しないのは actor の使い方から外れるためにこのようになっているのではないかと思います。
```swift
actor Counter {
    nonisolated let id: String
    nonisolated var count: Int = 0 // ❗️ 'nonisolated' can not be applied to stored properties 
    nonisolated var p1: String { "p1" }
    nonisolated func f1() {}
}
```

また、 `let` プロパティは `nonisolated` にすることは可能ですが、おそらく実際にするメリットはあまりないのではないかと思っています。`let` にはデフォルトで actor 外から同期的にアクセスできるようになっているためです。これは、データ競合が起こるには同じデータに同時にアクセスが発生しそのうち１つが書き込みである必要がありますが、 `let` には書き込みができないためデータ競合が起きる余地がないのでそうなっているのだと思います。

```swift
actor Counter {
    let id: String
    
    init(id: String) {
        self.id = id
    }
}

@main
public struct Main {
    public static func main() async throws {
        let counter = Counter(id: "counter")
        print(counter.id) // ✅ OK。 nonisolated がついていなくても同期的にアクセスできる
    }
}
```

### `nonisolated` の使いどころ

`nonisolated` が役立つ場面として、例えば複数のスレッドから同時にアクセスしても安全な actor の computed プロパティに対して同期的にアクセスできるようにすることが挙げられます。
`Counter` の概要を文字列で返す以下の `description` プロパティを考えます。

```swift
actor Counter {
    let id: String
    var count: Int = 0
    
    var description: String {
        "Counter with ID \(id)"
    }
    
    init(id: String) {
        self.id = id
    }
}
```

`description` プロパティは actor に isolate されているので actor 外からは `await` をつけて呼ぶ必要があります。同期関数からそのまま呼べないため不便です。

```swift
@main
public struct Main {
    public static func main() async throws {
        let counter = Counter(id: "counter")
        print(counter.description) // ❗️ Expression is 'async' but is not marked with 'await'
    }
}
```

しかし、 `description` の実装を冷静に見直してみると `let` プロパティである `id` にしか依存していません。先ほども説明したように `let` プロパティではデータ競合が発生せず複数スレッドから同時にアクセスしても安全なため、 `description` も同様に複数スレッドから同時にアクセスできることになり、 actor の保護が必要ないということになります。つまり、 `description` は actor に isolate する必要がないのにも関わらず isolate されているせいで非同期にしかアクセスできず不便になっていることになります。このような状況で `description` に `nonisolated` をつけて actor に isolate されなくすることで、同期的にアクセスすることが可能になります。

```swift
actor Counter {
    let id: String
    var count: Int = 0
    
    nonisolated var description: String {
        "Counter with ID \(id)"
    }
}

@main
public struct Main {
    public static func main() async throws {
        let counter = Counter(id: "counter")
        print(counter.description) // ✅ OK
    }
}
```
ここで、もちろん actor に isolate するべきプロパティに対して `nonisolated` な処理からアクセスできてしまうとデータ競合の原因になります。そのような危険なアクセスに対してはちゃんとコンパイラがエラーを出してくれるため、我々は安心して `nonisolated` を使うことができます。
```swift
actor Counter {
    let id: String
    var count: Int = 0

    nonisolated var description: String {
        // ❗️ Actor-isolated property 'count' can not be referenced from a non-isolated context
        "Counter with ID \(id) with count \(count)"
    }
}
```
`nonisolated` が役立つ別の場面として、 actor を特定の protocol に準拠させたい時があります。例えば、 `Counter` を [`CustomStringConvertible`](https://developer.apple.com/documentation/swift/customstringconvertible) に準拠させたいとします。 `CustomStringConvertible` の要求は
```swift
public protocol CustomStringConvertible {
    var description: String { get }
}
```
のみですが、以下のような `Counter` の実装だと準拠がうまくいかずエラーになってしまいます。
```swift
actor Counter: CustomStringConvertible {
    let id: String

	// ❗️ Actor-isolated property 'description' cannot be used to satisfy nonisolated protocol requirement
    var description: String {
        "Counter with ID \(id)"
    }
}
```
これは、 `description` が isolate されていることから actor 外からは非同期に呼ぶ必要があるため、インターフェースとしては `async` なプロパティになっているためです。 `CustomStringConvertible` の要求として `description` に同期的にアクセスできないといけないことになっているため、非同期にしかアクセスできないようではこの要求を満たすことができません。
このような場面で `nonisolated` が有効です。 `description` に `nonisolated` をつけることで actor 外からも同期的にアクセスできるようになるため、 `CustomStringConvertible` の要求を満たすことができます。

```swift
// ✅ OK
actor Counter: CustomStringConvertible {
    let id: String

    nonisolated var description: String {
        "Counter with ID \(id)"
    }
}
```

`CustomStringConvertible` 以外にも、例えば `Identifiable` や `Hashable` などほとんどの Swift の組み込みの protocol は同期的なプロパティやメソッドを要求するため、 actor を何らかの protocol に準拠させる時には `nonisolated` を使う場面が多くなるでしょう。

## まとめ
- `isolated` キーワードを関数のパラメータの actor につけることで、関数をその actor に isolate することができる
- `nonisolated` キーワードを actor のメンバーにつけることで isolate しないことができる
	- actor 外から同期的にアクセスしたい場合や、 actor を特定の protocol に準拠させたいときに有効

## 参考

- [SE-0313 Improved control over actor isolation](https://github.com/apple/swift-evolution/blob/main/proposals/0313-actor-isolation-control.md)