📖

パート2 : TCAでactionをsendして実行されるまで

2023/08/25に公開

StoreTask vs ViewStoreTask

ViewStoreのsendはこんな感じの実装でした。

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/ViewStore.swift#L207-L210

ここではStoreTaskを返します。

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/Store.swift#L853-L877

StoreTaskのコードを見ているとどうも最近生えたようなのでPRを確認してみます。

https://github.com/pointfreeco/swift-composable-architecture/pull/2241

コメントを見る限り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で

https://github-com.translate.goog/pointfreeco/swift-composable-architecture/pull/2222?_x_tr_sl=auto&_x_tr_tl=ja&_x_tr_hl=ja

この文脈はPRにも書かれているとおり下を見れば良さそうです。

https://github.com/pointfreeco/swift-composable-architecture/discussions/2223

🪦 R.I.P. ViewStore, 2020–2023

そういうことです。ViewStoreはもうすぐその役目を終えるようです。

そういえばこんなツイートがありましたが、Obserbationフレームワークがその代わりを担うようです。楽しみですね。

https://twitter.com/pointfreeco/status/1667248728862871612?s=20

Store.send

sendを見ます。

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/Store.swift#L427-L431

冒頭のThreadCheckはDEBUGで括られているので割愛します。

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/Store.swift#L573-L651

BoxとbufferdAction

前回のStoreのメンバ変数の確認で見たとおり、sendの最初にactionをbufferdActionに溜めます。

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/Store.swift#L434-L435

isSendingフラグを抜けることができるのは、defer内で変更された時です。

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/Store.swift#L437-L453

actionの処理に移る前にこのTaskを格納しているBoxの役割ですが_read & _modifyによってラップした値を返したい意図なので、不要なコピーを避けたいって感じでしょうか。コピーコストが高いものをmoveしたり、参照で取得したいというのはC++ぽさも感じます。結構便利なので積極的に使ってくのが良さそうな感じがします。

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/Internal/Box.swift#L1-L12

_read & _modifyは所有権周りの議論にも出てきていた記憶ですが脱線しそうなので別途書きます。

https://forums.swift.org/t/a-roadmap-for-improving-swift-performance-predictability-arc-improvements-and-ownership-control/54206

再入可能なactionの処理

さて、deferの中身は元々非常にシンプルだったようですが、このPRによって再入可能なactionを対応する際に今の形になったようです。

https://github.com/pointfreeco/swift-composable-architecture/commit/5b78fbcb0583568392762b15a262b3106cfb5185

再入可能なactionについてはこのQ&Aをみると実装経緯が分かりそうなのでみていきます。

https://github.com/pointfreeco/swift-composable-architecture/discussions/1698

質問の趣旨は太字で示されているとおり、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を使用するのが主流な感じがしているので具体例がまだ思いついてないです。

https://github.com/pointfreeco/swift-composable-architecture/discussions/2050

reduceの実行

https://github.com/pointfreeco/swift-composable-architecture/blob/6dfdd189064f96fb2543265cf55148ec731c70b3/Sources/ComposableArchitecture/Store.swift#L455-L459

459行目でようやくreduderに到達しました。ここにあるreducer.reduceから私たちが普段目にするReducerの処理が呼ばれます。

ただ、実際にはもう少し曲がりくねっていてScopedReducerを見る必要があります。その後の処理もまだ続くため一旦ここで区切って、次のパートに繋いでいこうと思います。

Discussion