【TCA】Reducerの共有ロジックの実装パターン

# アクションを送信して共有ロジックを呼び出すのはアンチパターン
[公式ドキュメント](https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance#Sharing-logic-with-actions)で言及されていますが、**共有ロジックを呼び出すためにアクションを送信するという実装パターンは非推奨**とされています。
TCAにおいてアクションを送信するというのはパフォーマンス観点で高いコストを伴う処理であるためです。

以下は公式から引用したコードですが、`.sharedComputation`というアクションに共有ロジックが実装されており、この共有ロジックを使用するために他のアクションから`.sharedComputation`アクションが送信されています。

```swift
switch action {
case .buttonTapped:
    state.count += 1
    return .send(.sharedComputation)

case .toggleChanged:
    state.isEnabled.toggle()
    return .send(.sharedComputation)

case let .textFieldChanged(text):
    state.description = text
    return .send(.sharedComputation)

case .sharedComputation:
    // Some shared work to compute something.
    return .run { send in
        // A shared effect to compute something
    }
}
```

# ReducerにEffectを返すメソッドを実装する
ではどうすればいいかというと、ReducerにEffectを返すメソッドを実装します。
これは公式ドキュメントで推奨されている実装パターンです。

`sharedComputation`はReducerのメソッドなので、すべての依存関係にアクセスができます。
また、`inout State`としてStateを受け取ることで、メソッド内でStateの更新も可能です。

```swift
func reduce(into state: inout State, action: Action) -> Effect<Action> {
    switch action {
    case .buttonTapped:
        state.count += 1
        return self.sharedComputation(state: &state)
    
    case .toggleChanged:
        state.isEnabled.toggle()
        return self.sharedComputation(state: &state)
    
    case let .textFieldChanged(text):
        state.description = text
        return self.sharedComputation(state: &state)
    }
}
    
func sharedComputation(state: inout State) -> Effect<Action> {
    // Some shared work to compute something.
    return .run { send in
        // A shared effect to compute something
    }
}
```

これは非常に有用な実装パターンですが、もう一つ、共有ロジックをより柔軟に扱える実装パターンをご紹介します。

# ReducerにSendを引数に持つメソッドを実装する
Effectを返すメソッドは、Effect.runのクロージャ内で呼び出すことができません（呼び出すことができるにはできるんですが、※の箇所が実行されません）。
このため、例えば複数の共有ロジックを実行したいときに困ります。

:::note
[Effect.mergeを使えばできるのではないか？](https://qiita.com/takehilo/items/00ea8c0ccb941d0a5fa5#comment-b4bcfa93c52ec22332b7)というご意見をいただいたので、別のユースケースを[こちら](
https://qiita.com/takehilo/items/00ea8c0ccb941d0a5fa5#comment-b4bcfa93c52ec22332b7
)に書きました。合わせてご参照ください。
:::

```swift
func reduce(into state: inout State, action: Action) -> Effect<Action> {
    switch action {
    case .button1Tapped:
        return sharedLogic1(state: state)  // ここはOK

    case .button2Tapped:
        // どうやってsharedLogic1とsharedLogic2の2つのメソッドを実行する？

        // 例えばこうすると
        return .run { [state] send in
            sharedLogic1(state: state)  // ここで使おうとするとNG
            sharedLogic2(state: state)
        }
    }
}

func sharedLogic1(state: State) -> Effect<Action> {
    // ここは実行される
    print("sharedLogic1")

    return .run { send in
        // ※ここが実行されない
        await send(.fetchUserResponse(TaskResult {
            try await userClient.fetch(state.userId)
        }
    }    
}

func sharedLogic2(state: State) -> Effect<Action> { ... }
```

そこで、Sendを引数に持つasyncなメソッドに変更します。
こうすると、場所を選ばず柔軟に共有ロジックを呼び出すことができるようになります。

```swift
func reduce(into state: inout State, action: Action) -> Effect<Action> {
    switch action {
    case .button1Tapped:
        return .run { [state] send in
            await sharedLogic1(send: send, state: state)  // ここで使える
        }

    case .button2Tapped:
        return .run { [state] send in
            await sharedLogic1(send: send, state: state)  // ここでも使える
            await sharedLogic2(send: send, state: state)
        }
    }
}

func sharedLogic1(send: Send<Action>, state: State) async {
    await send(.fetchUserResponse(TaskResult {
        try await userClient.fetch(state.userId)
    }
}

func sharedLogic2(send: Send<Action>, state: State) -> async { ... }
```

ただし、この実装パターンではメソッド内でStateを更新することはできません。
非同期コンテキスト内ではミュータブルな変数をキャプチャすることができないからです。

```swift
func reduce(into state: inout State, action: Action) -> Effect<Action> {
    switch action {
    case .buttonTapped:
        return .run { send in
            // Mutable capture of 'inout' parameter 'state' is not allowed in concurrently-executing code
            await sharedLogic(send: send, state: &state)
        }
    }
}

func sharedLogic(send: Send<Action>, inout state: State) async {
    state.count += 1
    await send(.fetchUserResponse(TaskResult {
        try await userClient.fetch(state.userId)
    }
}
```

なので、どのようにロジックを共通化するかによって、上で挙げた実装パターンを使い分けることになるかと思います。

なお、Sendを使った実装パターンは[isowords](https://github.com/pointfreeco/isowords/blob/763491628dbd5af5225ae5dc7a94de6c3777e73b/Sources/HomeFeature/Home.swift)でも使用されています。