All Articles

関数をプロパティの登録するテクニックを使ってReduxを実装してみる

はじめに

ClassやStructのプロパティに値を保持することは一般的ですが、関数をプロパティに登録することも可能です。 関数をプロパティに登録することで、関数の登録と実行を分離することができます。 これを使ってReduxライクなプログラミングをしてみたいと思います。

単一関数を登録する簡単な例

まず最もシンプルな任意の単一関数をプロパティに登録する例を見ていきましょう。

struct Store {
    var result: Int
    var fn: (inout Store, Int) -> Void
    init(initialValue: Int, fn: @escaping (inout Store, Int) -> Void) {
        self.result = initialValue
        self.fn = fn
    }
    func execute(store: inout Store, value: Int) {
        fn(&store, value)
    }
}

コンストラクタに関数を受け渡し登録します。 そして登録した関数は、 executeで後から実行します。

関数だけでなく、クロージャーも登録可能です。関数とクロージャーを区別する必要はありません。

// 関数
func add(store: inout Store, value: Int) {
    let calc = store.result + value
    store.result = calc
}

// 関数
func sub(store: inout Store, value: Int) {
    let calc = store.result - value
    store.result = calc
}

// クロージャー
var multi: (inout Store, Int) -> Void = { (store, value) in
    let calc = store.result * value
    store.result = calc
}
// 足し算
var store1 = Store(initialValue: 2, fn: add)
store1.execute(store: &store1, value: 5) // 7

// 引き算
var store2 = Store(initialValue: 2, fn: sub)
store2.execute(store: &store2, value: 5) // -3

// 掛け算
var store3 = Store(initialValue: 2, fn: multi)
store3.execute(store: &store3, value: 5) // 10

Storeの初期化時に実行する関数を受け取っています。そして、渡した関数を実行するために、executeを呼び出します。今回はすごくシンプルな例ですが、Dependency Injectionのコンストラクタインジェクション時に関数登録を行うことで、それ以降の処理を共通化できます。

Reduxの仕組みを実装

こちらはより実践的な例です。Reduxの仕組みを作ってみましょう。

上の例は単一関数を登録していましたが、add, sub, multi の機能を持たせるためにEnumを活用します。

enum AppAction {
    case calculation(CalculationAction)
    // 別のグループの関数を登録したい場合はcaseを追加
}

enum CalculationAction {
    case add(Int)
    case sub(Int)
    case multi(Int)
}
func appReducer(state: inout AppState, action: AppAction) {
    switch action {
    case let .calculation(.add(value)):
        let num = state.value + value
        state.value = num
    case let .calculation(.sub(value)):
        let num = state.value - value
        state.value = num
    case let .calculation(.multi(value)):
        let num = state.value * value
        state.value = num
    }
}

続いて、Reducer、State、そして登録した関数を実行する(execute関数)を持つStoreを作成します。

struct Store {
    private(set) var appState: AppState
    let reducer: (inout AppState, AppAction) -> Void
    init(initialState: AppState, reducer: @escaping(inout AppState, AppAction) -> Void) {
        self.appState = initialState
        self.reducer = reducer
    }
    mutating func execute(action: AppAction) {
        reducer(&appState, action)
    }
}

今回の例では、計算結果のみですが、Structにしているので、別のプロパティを持つことができます。

struct AppState {
    var value = 0
}
  • 関数呼び出し
var store = Store(initialState: AppState(value: 2), reducer: appReducer(state:action:))

コンストラクタで初期値とReducerを登録しています。

そして、関数実行

store.execute(action: .calculation(.add(5))) // 7
store.execute(action: .calculation(.sub(1))) // 6
store.execute(action: .calculation(.multi(2))) // 12

このように関数をプロパティに登録するテクニックを覚えれば、Reduxの仕組みも簡単に実装できます。 Swiftで書くとすごく簡潔に書けますね!

SwiftUIで使う形に加工

StoreをObservableObject に準拠させて、appStateに@Publishedを追加すればSwiftUIでReduxとして利用可能です。

struct Store: ObservableObject {
    @Published private(set) var appState: AppState
    let reducer: (inout AppState, AppAction) -> Void
    init(initialState: AppState, reducer: @escaping(inout AppState, AppAction) -> Void) {
        self.appState = initialState
        self.reducer = reducer
    }
    mutating func execute(action: AppAction) {
        reducer(&appState, action)
    }
}

Closureのキャプチャーを利用してReducerを分割

上のappReducerは、今回のような、足し算、引き算、掛け算なら、1つの関数として定義すれば良いとは思いますが、処理が複雑になってコードが長くなると、1つの関数にしておくことは可読性を損ねるでしょう。

今回は、Closureコンテキスト内に定義している値をキャプチャーするという特徴を使って、appReducerを分割していきます。

例をシンプルにするために、AppActionの直下に、CalculationActionを挟まず、add, sub, multiのアクションを移動します。

ソース全文

enum AppAction {
    case add(Int)
    case sub(Int)
    case multi(Int)
}

struct AppState {
    var value = 0
}

struct Store {
    var appState: AppState
    let reducer: (inout AppState, AppAction) -> Void
    init(initialState: AppState, reducer: @escaping(inout AppState, AppAction) -> Void) {
        self.appState = initialState
        self.reducer = reducer
    }
    mutating func execute(action: AppAction) {
        reducer(&appState, action)
    }
}

func combine(
  _ reducers: (inout AppState, AppAction) -> Void...
) -> (inout AppState, AppAction) -> Void {
  return { state, action in
    for reducer in reducers {
      reducer(&state, action)
    }
  }
}

func addReducer(state: inout AppState, action: AppAction) {
    switch action {
        case let .add(value):
            state.value = state.value + value
        default:
            break;
    }
}

func minusReducer(state: inout AppState, action: AppAction) {
    switch action {
        case let .sub(value):
            state.value = state.value - value
        default:
            break;
    }
}

func multiReducer(state: inout AppState, action: AppAction) {
    switch action {
        case let .multi(value):
            state.value = state.value * value
        default:
            break;
    }
}

let appReducer = combine(addReducer(state:action:)
                         ,minusReducer(state:action:)
                         ,multiReducer(state:action:))

var store = Store(initialState: AppState(value: 2), reducer: appReducer)
store.execute(action: .add(5))
store.execute(action: .sub(1))
store.execute(action: .multi(2))

Store

この部分には CalculationAction を削った以外変更はありません。

enum AppAction {
    case add(Int)
    case sub(Int)
    case multi(Int)
}

struct AppState {
    var value = 0
}

struct Store {
    var appState: AppState
    let reducer: (inout AppState, AppAction) -> Void
    init(initialState: AppState, reducer: @escaping(inout AppState, AppAction) -> Void) {
        self.appState = initialState
        self.reducer = reducer
    }
    mutating func execute(action: AppAction) {
        reducer(&appState, action)
    }
}

appReducer

appReducerを分割して以下の関数を用意します。

func addReducer(state: inout AppState, action: AppAction) {
    switch action {
        case let .add(value):
            state.value = state.value + value
        default:
            break;
    }
}

func minusReducer(state: inout AppState, action: AppAction) {
    switch action {
        case let .sub(value):
            state.value = state.value - value
        default:
            break;
    }
}

func multiReducer(state: inout AppState, action: AppAction) {
    switch action {
        case let .multi(value):
            state.value = state.value * value
        default:
            break;
    }
}

特に難しいところはありませんが、 default: を追加することでcaseを分割している点を覚えておいてください。

分割した関数を統合するcombine関数を作成してみましょう。

func combine(
  _ reducers: (inout AppState, AppAction) -> Void...
) -> (inout AppState, AppAction) -> Void {
  return { state, action in
    for reducer in reducers {
      reducer(&state, action)
    }
  }
}

目が慣れるまでに読みにくい文法なので、一行ずつ分解していきましょう。

reducers: (inout AppState, AppAction) -> Void...

これは可変長引数を受け取るために ... を末尾につけています。 今回は、addReducerminusReducermultiReducerを受け取れるインターフェイスとなっています。

戻り値に注目してみましょう。

appReducerは、のインターフェイス

func appReducer(state: inout AppState, action: AppAction) {
  ...
}

combine は、高階関数の形をしていて、戻り値はappReducerと同様のインターフェイスをしています。

-> (inout AppState, AppAction) -> Void {

Closureコンテキスト内に定義している値をキャプチャーする

return { state, action in
   // Closureコンテキスト内
}
  return { state, action in
    for reducer in reducers {
      reducer(&state, action)
    }
  }

reducers ※ は、Closureコンテキスト内に記述されているため、キャプチャーされます。 ※つまり、可変長引数として渡された、addReducerminusReducermultiReducerがキャプチャーされます。

これで、引数として、state, actionを受け取るとキャプチャーされた、reducersとして登録したReducerを1つずつ実行されていきます。actionが一致するものだけが実行され、一致しないものは、default: を通過して無視されます。

  return { state, action in
    for reducer in reducers {
      reducer(&state, action)
    }
  }

最後に、combineを使ってappReducerを作成しています。

let appReducer = combine(addReducer(state:action:)
                         ,minusReducer(state:action:)
                         ,multiReducer(state:action:))

Closureの値キャプチャー(今回は関数キャプチャー)は非常に強力ですね。