The Composable ArchitectureでPullbackは何をしているのか

# はじめに
最近SwiftUIの登場とともに注目を集めている設計パターン[The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)（以下TCA）を理解するにあたって、EffectやStoreなど重要な機能がある中でPullbackが行う処理について理解に少し時間がかかったため詳細をまとめようと思いました。

# 対象

- TCAの概要は理解した方
- Pullbackやcombineへの理解を深めたい方
- [Point-Freeで1時間かけない](https://www.pointfree.co/episodes/ep69-composable-state-management-state-pullbacks#t1263)でざっと復習したい方（絶対見た方が良い）

#### TCAをまだ知らない方
もしまだTCAにガッツリ触れられていない場合は以下のサイトをおすすめします。

- [Learn More at pointfreeco/swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture#learn-more)
- [Swiftによるアプリ開発のためのThe Composable Architectureがすごく良いので紹介したい](https://qiita.com/yimajo/items/77c204ab091223f9cb14)

# Pullbackは何をしているのか
**それぞれ小さく分割されているReducerの`State`と`Action`を取り出してグローバルに使える`AppState`と`AppAction`へ型を変更することで、 別々のReducerを統合できる形にしています。**

どうしてそもそもグローバルに使えるReducerに変更するのか。
Stateをどうやって取り出すのか。
Actionをどうやって取り出すのか。

これらを見ていきたいと思います。

# 小さく分かれたReducer
シンプルなカウントアップアプリをサンプルとして見ていきます。（分かりやすくするために[Point-Freeのカウントアップアプリ](https://www.pointfree.co/episodes/ep69-composable-state-management-state-pullbacks#t101)と機能を少し変えています）

機能
- カウントアップ・カウントダウン
- 素数の判定
- お気に入り登録

## AppReducer

さて、いわゆるアプリのビジネスロジックを全て受け持つ`appReducer`があるとします。

```swift
func appReducer(value: inout AppState, action: AppAction) -> Void {
  switch action {
  case .counter(.decrTapped):
    // カウントダウン

  case .counter(.incrTapped):
    // カウントアップ

  case .primeModal(.calculateIsPrimeTapped):
    // 素数判定

  case .primeModal(.saveFavoritePrimeTapped):
    // 素数判定した数字をお気に入りに登録

  case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
    // お気に入りから削除
  }
}
```

`AppAction`は３つのcase（`.counter`,`.primeModal`,`.favoritePrimes`）を持っていることが分かります。
それぞれのActionが別画面から発火されるものであるとしたら、将来的な`appReducer`の肥大化も見据えて**各機能ごとにReducerを分割したい**のが自然な流れだと思います。

## appReducerを分割する
例えばcounterReducerであればカウント時に必要なStateとAction以外は不必要です。
つまり、Reducerを分割する際に各Reducerがグローバルな`AppState`と`AppAction`を持っていたらそのReducerの役割に対して持っている値の影響範囲が広すぎます。
そのため、それぞれ個別のStateとActionに切り出します。

```swift
func counterReducer(value: inout CounterState, action: CounterAction) -> Void {}
func primeModalReducer(value: inout PrimeModalState, action: PrimeModalAction) -> Void {}
func favoriteReducer(value: inout FavoriteState, action: FavoriteAction) -> Void {}
```

## 分割したReducerを統合する
分割したは良いものの、最終的には`AppState`と`AppAction`を受け取る`appReducer`として分割したReducerを統合しなければいけません。
そこで各Reducerを統合するのに使われるのが`combine`です。

```swift

func combine<Value, Action>(
// Reducerはいくつでも受け渡すことが出来る
  _ reducers: (inout Value, Action) -> Void...
) -> (inout Value, Action) -> Void {
  return { value, action in
    for reducer in reducers {
      reducer(&value, action)
    }
  }
}

// But..
let appReducer = combine(
  counterReducer,
  primeModalReducer,
  favoriteREducer
)
// Error - Cannot convert value
```
しかし問題があります。`combine`する各Reducerの`Value, Action`は同じでなければ`combine`出来ません。
つまり、**現状`CounterState`や`FavoriteState`に分かれている値を最終的に`AppState`と`AppAction`の型に変更して統合可能な型にしなければいけません。**

そこで行うのがPullbackの処理です。

# StateのPullback
まずはStateのPullbackを見ていきます。
`AppState`の中にそれぞれのStateをComputed Propertyとして追加します。

```swift
struct AppState {
  var counterState: CounterState {
    get { //.. }
    set { //.. }
  }
  var primeModalState: PrimeModalState {
    get { //.. }
    set { //.. }
  }
  //..
}
```
こうすることで`counterState`が`WritableKeyPath`として取得出来るようになります。

```swift
// StateをPullbackする
func pullback<LocalValue, GlobalValue, Action>(
    // 渡されたreducerとそのLocalValue(State)
  _ reducer: @escaping (inout LocalValue, Action) -> Void,
  value: WritableKeyPath<GlobalValue, LocalValue>
) -> (inout GlobalValue, Action) -> Void {
  return { globalValue, action in
    reducer(&globalValue[keyPath: value], action)
  }
}

let appReducer = combine(
  pullback(counterReducer, value: \.counterState),
  pullback(primeModalReducer, value: \.primeModalState),
  //..
)
```
これで各ReducerのStateは違えど、統合できそうです。

しかし、この実装だとStateをPullbackしただけで**Actionに関しては同じ型であることが前提です。**
次にActionも含めてPullbackする実装をみていきましょう。

# ActionのPullback
ActionがStateと異なる点は、**Stateは`struct`でActionは`enum`であるという点です。**
つまりStateの時に使用したKeypathが使えません。

そこで、enumでのKeypathを可能にするのが同じくPoint-Freeが公開しているOSS、[pointfreeco/swift-case-paths](https://github.com/pointfreeco/swift-case-paths)です。

```swift
// StateとAction両方をPullbackする
func pullback<GlobalValue, LocalValue, GlobalAction, LocalAction>(
  _ reducer: @escaping (inout LocalValue, LocalAction) -> Void,
  value: WritableKeyPath<GlobalValue, LocalValue>,
  action: CasePath<GlobalAction, LocalAction>
) -> (inout GlobalValue, GlobalAction) -> Void {
  return { globalValue, globalAction in
    guard let localAction = action.extract(from: globalAction) else { return }
    reducer(&globalValue[keyPath: value], localAction)
  }
}

enum AppAction {
  case counter(CounterAction)
  case primeModal(PrimeModalAction)
  //..
}

let appReducer = combine(
  pullback(counterReducer, value: \.counterState, action: \.counter)
  pullback(primeModalReducer, value: \.primeModalState, action: \.primeModal)
  //..
)
```
これでやっとStateとActionの両方を**`AppState`と`AppAction`へPullbackすることが出来ました。**

# そもそもPullbackの実装について
Pullbackは`map`と逆の処理を行うため、元々Point-Freeが`contramap`として定義していたものを直感的に分かりやすくするために[`Pullback`へ命名変更しました。](https://www.pointfree.co/blog/posts/22-some-news-about-contramap)
圏論の[「引き戻し」](https://ja.wikipedia.org/wiki/%E5%BC%95%E3%81%8D%E6%88%BB%E3%81%97_(%E5%9C%8F%E8%AB%96))= Pullbackにコンセプトが基づいているため、この名前に決定されました。
この記事ではここまでですが、そもそもPullbackの思想が気になる方は以下の記事を読むと面白いかもしれません。

- [Understanding contravariance](https://typeclasses.com/contravariance)
- [Contravariance by Point-Free](https://www.pointfree.co/episodes/ep14-contravariance#references)

以上は自分の理解になりますので、もしご指摘等あればコメントいただけると喜びます。

# 参照

- [The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)
- [Composable State Management: State Pullbacks by Point-Free](https://www.pointfree.co/episodes/ep69-composable-state-management-state-pullbacks)
- [Composable State Management: Action Pullbacks by Point-Free](https://www.pointfree.co/episodes/ep70-composable-state-management-action-pullbacks)