Closed7

SE-0401: Remove Actor Isolation Inference caused by Property Wrappers

UeeekUeeek

Introduction

SE-0316: Global Actors
GlocalActorはSE-316で導入された アノテーションで(e.g. @MainAcotr)
特定のglobalActorに型や関数やpropertyを隔離するために使用する。
その時に、GlobalActorへの隔離が どのように推論されるかのルールが導入された

GlobalActorやnonisolatedと明示的に宣言されてない場合でも、globalActor isolationとして推論される。

[...]

  • GlobalActorのついたpropertyを持つwrapped instanceを持つ、structやclassは、propertyWrapperのActorに隔離されていると推論される。

    @propertyWrapper
    struct UIUpdating<Wrapped> {
      @MainActor var wrappedValue: Wrapped
    }
    
    struct CounterView { // infers @MainActor from use of @UIUpdating
      @UIUpdating var intValue: Int = 0
    }
    

この提案では、Swift6で、そのルールを取り除くものである。
上の例では、CounderViewは @MainActor isolationではなくなる。

UeeekUeeek

Motivation

この推論のルールは 多くのユーザにとって わかりにくい。
いつActor isolationが適応されるかがわかりずらいため、何人かの開発者はSwiftConcurrencyを理解するのが難しいと感じている。
何かが推論される時には、それはユーザにとってはみえない。そして、理解を困難にする。
これは、SwiftUIで提供されているPropertyWrapperを使用する時に、よく問題になる。また、それだけに限られた問題ではない。

UeeekUeeek

An example using SwiftUI

struct MyView: View {
  // Note that `StateObject` has a MainActor-isolated `wrappedValue`
  @StateObject private var model = Model()
  
  var body: some View {
    Text("Hello, \(model.name)")
      .onAppear { viewAppeared() }
  }

  // This function is inferred to be `@MainActor`
  func viewAppeared() {
    updateUI()
  }
}

@MainActor func updateUI() { /* do stuff here */ }

上のコードは動く。が、StateObjectをStateに変更するとエラーになる。

-   @StateObject private var model = Model()
+   @State private var model = Model()
  func viewAppeared() {
    // error: Call to main actor-isolated global function
    // 'updateUI()' in a synchronous nonisolated context
    updateUI()
  }

Stateにすることで、その間するがmodelを使用してないにも関わらず、 viewAppeared()がコンパイルエラーになる。
propertyの宣言を変えたことによって、それを使用してないところでエラーが発生するのは明らかではない。
実際、MyViewのActorもこの変更によって影響を受けている

An example not using SwiftUI

この問題はSwiftUIだけでない。

// DB library用のPropertyWrapper
@propertyWrapper
struct DBParameter<T> {
  @DatabaseActor public var wrappedValue: T
}

// このstructはDatabaseActor isolatedと推論される。
struct DBConnection {
  @DBParameter private var connectionID: Int

  func executeQuery(_ query: String) -> [DBRow] { /* implementation here */ }
}


// In some other file...

@DatabaseActor
func fetchOrdersFromDatabase() async -> [Order] {
  let connection = DBConnection()

  // No 'await' needed here, because 'connection' is also isolated to `DatabaseActor`.
  connection.executeQuery("...")
}

connectionIDを取り除くことで、DBConnectionのActor Isolationの推論の結果代わり、fetchOrdersFromDatabaseがコンパイルエラーになる。
プライベート プロパティへの変更により、完全に別のファイルでコンパイル エラーが発生することは、Swift では前例のないことです
前述のactor isolationの推論(property wrapperからそれを持つ型)は、型の中のprivave propertiesであっても、その変更の及ぶ範囲を、型の中だけに限定することができない。
他のファイル内などで、compile errorが発生してしまう可能性がある。

Does this cause actual problems?

この挙動はcommunityで混乱を引き起こしている。
This behavior has caused quite a bit of confusion in the community. For example, see this tweet, this blog post, and this entire Swift Forums thread.
一つの引用をすると, this post,
この推論は、actor isolationの影響が想定より多くの範囲に及ぶため、SwiftConcurrencyの導入を、いくつかの場合で難しいものにしている。

class MyContainer {
     let contained = Contained() // error: Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context
}

class Contained {
    @OnMainThread var i = 1
}

作成者は、特定のプロパティがメインスレッドから分離されていることを宣言することを目的として、@OnMainThread プロパティ ラッパーを作成しました。
ただし、プロパティ ラッパー内で @MainActor を使用してこれを強制することはできません。これを行うと、含まれる型全体が予期せず分離されてしまうためです。
しかし。そのproperty wrapperではMainActorでの実行を強制することができない。

The original motivation
この推論ルールの元々の動機は、SwiftUI の @ObservedObject のようなプロパティ ラッパーを使用する際のアノテーションの負担を軽減することでした。
しかし、それが実際に何かを大幅に容易にするかどうかは明らかではありません。型に 1 つのアノテーションを記述する必要がなくなるだけであり、そのアノテーションが失われると、最小の驚きの原則に違反することになりますprinciple of least surprise.

UeeekUeeek

Proposed solution

提案は単純です。Swift 6 言語モードでは、型内で使用されるプロパティ ラッパーは、型のアクター分離に影響を与えません。この推論ステップを完全に無効にするだけです。

Swift 5 言語モードでは、分離は現状と同様に推論され続けます。新しい動作は、-enable-upcoming-feature DisableOutwardActorInference コンパイラ フラグを使用して有効にできる。

Detailed design

ActorIsolationRequest.getIsolationFromWrappers() が議論している推論ルールを実装している。
この関数が、Swift6やflagを有効にしているときに、推論を行わないように調整される。

UeeekUeeek

Source compatibility

この変更は破壊的変更になる可能性がある。なぜなら、propertyWrapperのActors推論ルールに依存したコードが存在するかもしれないからである。
現状でも、コードは、明示的にGlobalActorをつけて宣言することができる。
例えば、その型がMainActorと推論されていても、それを明示的に宣言するとこができる。
明示的につけていれば推論されないので、今回の影響を受けない(影響があっても、わかりやすい?)

破壊的変更はlibrary の著者が軽減することもできる。
例えば、AppleがSwiftUI.ViewをMainAcotrにすると、Viewに準拠する型はMainActorに一貫して隔離されることになる。(vs. property wrapperのactorによって隔離されるactorが変わる現状の挙動と比べて)
この提案では 影響を軽減する方法があることを提示することだけ行い、そうするように進めているわけではない。

Source compatibility evaluation

この返答の影響を推測するために、macOS toolchainを使用して open-source swift projectへの影響を評価した。
結果、この変更による実際のコードへの影響は見つからなかった。
多くのOSSは property wrapperを使用してない。
少数のライブラリはproperty wrapperを使用しているので、そこへの影響を下に記述する。

Project Outcome Notes
ACNHBrowserUI Fully Compatible Uses SwiftUI property wrappers
AlamoFire Fully Compatible Uses custom property wrappers, but none are actor isolated
Day One (Mac) Fully Compatible Uses SwiftUI property wrappers. (Not open source)
Eureka Fully Compatible Does not use property wrappers at all
NetNewsWire Fully Compatible Uses SwiftUI property wrappers
swift-nio Fully Compatible Does not use property wrappers at all
SwiftyJSON Fully Compatible Does not use property wrappers at all
XcodesApp Fully Compatible Uses SwiftUI property wrappers

上の全てのライブラリは、ConcurrencyCheckをminimalにしている。
Targetに変更しても、上の全てはerrorにはならなかった。
今回の変更を試してもエラーにはならなかった。

Completeに変更すると、ほとんどのライブラリはエラーになった。(今回の提案を有効にしても、しなくても)
この変更は、Complete modeでいくつかのエラーを生じている可能性があるが、ソース互換性が保たれていたはずのプロジェクトのソース互換性が損なわれることはありませんでした。

Effect on ABI stability

No impact on ABI.

Effect on API resilience

No impact on API resilience

UeeekUeeek

コンパイルエラーでわかるので、治しやすそう?

このスクラップは2024/04/27にクローズされました