🏄

TCA v1.5.4のパフォーマンスに親が子を呼び出すドキュメント追加されていた

2023/12/15に公開

はじめに

TCAのv1.5.4でパフォーマンスに関するドキュメントに追記されてたことが興味深かった。

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

要約

  • 前提
    • Actionを共有する場合にReducerのActionを呼び出すと無駄がある
  • どうするか
    • Actionでなく関数にして共有する
    • 応用
      • 配列でもいける

実例

Before/Afterで説明する。

Before

// Handling action from parent feature:
case .buttonTapped:
  // Send action to child to perform logic:
  return .send(.child(.refresh))

親子のReducerが合成されていて、親が子のReducer経由でActionを実行している。

これを次のように改善している。

After

case .buttonTapped:
  return Child().reduce(into: &state.child, action: .refresh)
    .map(Action.child))

Childをインスタンス化し、reduce(into:, action:)メソッドで呼び出している。
これで合成されていない子Reducerを呼び出せる。

mapしているのは、親(呼び出し元)のEffectに変換している。そうしないと子ReducerのEffectになっているためだろう。

何がしたいのか

そもそもSharing logic with actionsのドキュメントから

https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/performance/#Sharing-logic-with-actions

  • ReducerでActionを送信するコストは比較的にある
    • Actionを送信すると複数のレイヤーを通過しReducerがActionを解釈する
      • 例ではReducerで共通化処理をしているActionをやめ、RedcuerのActionを介さずに単純にメソッドとして共通の処理を関数化している

1つのReducer同士で処理が共通化している場合の実例がある。

実例

@Reducer
struct Feature {
  struct State { /* ... */ }
  enum Action { /* ... */ }


  var body: some Reducer<State, Action> {
    Reduce { state, action in
      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
    }
  }
}

応用を想像

子が配列の場合

子がIdentifiedArrayOfの場合ややこしくなるが、基本はmapする際に引数として子のActionが来るので親である自分のActionを返す。つまり(子のAction) -> (親のAction)のクロージャを渡せばいい。

public struct ContainerFeature {
    public struct State: Equatable {
        var array: IdentifiedArrayOf<WeekFeature.State>
    ...
    }

    public enum Action: ViewAction, BindableAction {
        case array(IdentifiedActionOf<WeekFeature>)
    ...
    }

    public var body: some Reducer<State, Action> {
         switch action {
         case なにか:
             let id = ...
             return WeekFeature().sharedComputation(
                 state: &(state.array[id: id]!)
             )
             .map { (action: WeekFeature) in 
                 Action.array(.element(id: id, action: action))
             }
         }         
    }

整理

  • 1つのReducer同士で処理を共有化したい
    • ActionとしてReducerに定義するのではなく関数化する
  • 親と子で処理を共有化したい
    • 子をインスタンス化して合成してない状態でReducerのActionを呼び出す
  • 応用を想像: 1つのReducer同士で処理を共有下した処理を親が配列の子を呼び出したい
    • 子へidでアクセスし、Actionも引数とする

Discussion