パート2 : TCAでactionをsendして実行されるまで
StoreTask vs ViewStoreTask
ViewStoreのsendはこんな感じの実装でした。
ここではStoreTaskを返します。
StoreTaskのコードを見ているとどうも最近生えたようなのでPRを確認してみます。
コメントを見る限りv0.55で入った修正に追従した結果のようですね。
v0.55 deprecates the WithViewStore initialiser that takes Void state and recommends using Store.send instead, however there isn't parity with the ViewStore APIs as those do let you specify an animation or transaction parameter, while Store.send doesn't. This PR adds those methods for full parity
v0.55での変更と上のコメントが指しているのはこのPRで
この文脈はPRにも書かれているとおり下を見れば良さそうです。
🪦 R.I.P. ViewStore, 2020–2023
そういうことです。ViewStoreはもうすぐその役目を終えるようです。
そういえばこんなツイートがありましたが、Obserbationフレームワークがその代わりを担うようです。楽しみですね。
Store.send
sendを見ます。
冒頭のThreadCheckはDEBUGで括られているので割愛します。
BoxとbufferdAction
前回のStoreのメンバ変数の確認で見たとおり、sendの最初にactionをbufferdActionに溜めます。
isSendingフラグを抜けることができるのは、defer内で変更された時です。
actionの処理に移る前にこのTaskを格納しているBoxの役割ですが_read & _modifyによってラップした値を返したい意図なので、不要なコピーを避けたいって感じでしょうか。コピーコストが高いものをmoveしたり、参照で取得したいというのはC++ぽさも感じます。結構便利なので積極的に使ってくのが良さそうな感じがします。
_read & _modifyは所有権周りの議論にも出てきていた記憶ですが脱線しそうなので別途書きます。
再入可能なactionの処理
さて、deferの中身は元々非常にシンプルだったようですが、このPRによって再入可能なactionを対応する際に今の形になったようです。
再入可能なactionについてはこのQ&Aをみると実装経緯が分かりそうなのでみていきます。
質問の趣旨は太字で示されているとおり、TCAがシングルスレッドによってイベントを処理する前提に基づくと、一体どういう場合に前のsendが完了する前に別のsendが来るのかという点を聞いています。
Taking into account this serial nature of TCA I can't understand how yet another callee of send method can invoke it before a previous invocation of send is finished
Testコードにあるのですが、手元でも実行できるよう、前回示したコードを書き換えて、次のようにします。emitをSwiftUIのViewのtaskなどでsendした後、ボタンを押すとbufferdActionsが2個になることを確認できます。
struct AppReducer: Reducer {
let subject = PassthroughSubject<Void, Never>()
struct State: Equatable {
var count: Int = .zero
public init() {}
}
enum Action: Equatable {
case buttonTap
case emit
case increment
}
func reduce(into state: inout State, action: Action) -> ComposableArchitecture.Effect<Action> {
switch action {
case .buttonTap:
subject.send()
return .none
case .emit:
return .publisher {
subject.map {
.increment
}
}
case .increment:
state.count += 1
return .none
}
}
}
実務的にどういう場合に遭遇するかなーと考えるとSharedStateClientをAsyncStreamではなくて、なんらかの事情でPassthroughSubjectにしていたりすると起こるのかもしれないですAsyncStremを使用するのが主流な感じがしているので具体例がまだ思いついてないです。
reduceの実行
459行目でようやくreduderに到達しました。ここにあるreducer.reduceから私たちが普段目にするReducerの処理が呼ばれます。
ただ、実際にはもう少し曲がりくねっていてScopedReducerを見る必要があります。その後の処理もまだ続くため一旦ここで区切って、次のパートに繋いでいこうと思います。
Discussion