Type Friendly な Reducer 定義を考えて実装してみた

https://qiita.com/mizchi/items/0e2db7c56541c46a7785 を考え直す。
このパターン、明らかにまだ冗長で、しかもFlowもTSもこれでは推論器がよく落ちるという問題があった。
なのでそもそも型が自明なラッパーを定義するのがいいのでは、という発想からスタートしている。

追記: [hard-reducer という名前で実装した](https://github.com/mizchi/hard-reducer)

## 既存の reducer 定義の何が面倒臭いか

- 自明なことを何度も書く
- 定数値を何度も書いているようで結局 case 文で 一意に紐付いている
- Action の Union Type の推論が失敗こけやすい (flow も ts もすぐ型をロストする)
- 巨大 switch case

## 守るべき reducer の spec とは

- reducer の `(State, Action) => State` というインターフェース
    - 最終的に redux.combineReducers できればいいので
- 中間状態として JSON Serealizable な Action を正整する
- Flux Standard Action: `{ type: string, payload }`

## 既存実装の調査

redux-actions

```js
// Action
const { increment, decrement } = createActions({
  'INCREMENT': amount => ({ amount: 1 }),
  'DECREMENT': amount => ({ amount: -1 })
});

//Reducer
const reducer = handleActions({
  [increment](state, { payload: { amount } }) {
    return { counter: state.counter + amount }
  },
  [decrement](state, { payload: { amount } }) {
    return { counter: state.counter + amount }
  }
}, defaultState);
```

関数参照でマッチする。
動的に生成するには型が付けづらそうなインターフェースをしている。

typescript-fsa http://itexplorer.hateblo.jp/entry/20170720-typescript-fsa-preferavle-to-redux-actions

```js
// Action
const actionCreator = actionCreatorFactory();
export const increment = actionCreator<{amount: number}>('INCREMENT');
export const decrement = actionCreator<{amount: number}>('DECREMENT');

//Reducer
const reducer = reducerWithInitialState(defaultState)
  .case(increment, (action, amount) => ({ ...state, counter: state.counter + amount }))
  .case(decrement, (action, amount) => ({ ...state, counter: state.counter + amount }));
```

asyncCreator が内部で黒魔術をしていそう。内部でどういう風に action を組み立てるかは書けないようになっている。

## 今回のゴールの設定

redux-actions の関数参照でマッチするというスタイルと、 typescript-fsa の APIスタイルを真似つつ自明にする。

```js
// usage

const { createAction } = buildActionCreator({ prefix: 'counter/' })
const add = createAction('add', (val: number) => val)
const reducer = createReducer({ value: 0 })
  .case(add, (state, payload) => {
    const p: string = payload // ここの推論が落ちてほしい
    return {
      value: state.value + payload
    }
  })

const action = add(3) // => { type: 'counter/add', payload: 3 }
const ret = reducer({ value: 0 }, add(3))
```

- 何度も書くことで煩わしい prefix を付与しておく
  - FlowのUnionType では String を結合すると string 型にもどってしまうが、一回しか書かないなら問題ない
- createAction された add 関数はただの関数として振る舞う。
- 関数参照をkeyにコールバックを定義することで定数値を何度も書くのを省ける
  - 実際 switch case の中でも type refinement された後は ほぼ payload だけを触るんだから、既に type の値が自明なら `(State, Payload) => State` で 処理を書いていいはず

これに型がつくように実装すればよい

## どう実装するか

とりあえずで Flow で実装するとして

```
type ActionCreator<Constraint: string, Input, Payload> = {
  (Input): { type: Constraint, payload: Payload }
}

type Reducer<State> = {
  (State, any): State,
  get: () => Reducer<State>,
  case<Constraint, Input, Payload>(
    ActionCreator<Constraint, Input, Payload>,
    (State, Payload) => State
  ): Reducer<State>,
}

```

関数ではなく、 Callable な Object として定義して、メタ的に型情報を保存する。
`get: () => ...` の部分は Flow でそれ以外のメンバにアクセスできるようにするおまじない。アクセッサを踏んだ時にどう型が解決されるかだと解釈している。

```js
function buildActionCreator(opts: { prefix?: string }) {
  const prefix = opts.prefix || ''

  function createAction<Constraint: string, Input, Payload>(
    t: Constraint,
    fn: Input => Payload
  ): ActionCreator<Constraint, Input, Payload> {
    const type = prefix + t
    const fsaFn: any = (input: Input) => {
      return {
        type,
        payload: fn && fn(input)
      }
    }
    fsaFn._t = type
    return fsaFn
  }
  return { createAction }
}
```

ランタイム用のバックドアを仕込みつつ関数を返す。


## 実装

```js
/* @flow */
type ActionCreator<Constraint: string, Input, Payload> = {
  (Input): { type: Constraint, payload: Payload }
}

type Reducer<State> = {
  (State, any): State,
  get: () => Reducer<State>,
  case<Constraint, Input, Payload>(
    ActionCreator<Constraint, Input, Payload>,
    (State, Payload) => State
  ): Reducer<State>
}

function buildActionCreator(opts: { prefix?: string }) {
  const prefix = opts.prefix || ''

  function createAction<Constraint: string, Input, Payload>(
    t: Constraint,
    fn: Input => Payload
  ): ActionCreator<Constraint, Input, Payload> {
    const type = prefix + t
    const fsaFn: any = (input: Input) => {
      return {
        type,
        payload: fn && fn(input)
      }
    }
    fsaFn._t = type
    return fsaFn
  }

  function createPromiseAction<Constraint: string, Input, Payload>(
    t: Constraint,
    fn: Input => Promise<Payload> | Payload
  ): ActionCreator<Constraint, Input, Payload> {
    const type = prefix + t
    const fsaFn: any = (input: Input) => {
      return {
        type,
        payload: Promise.resolve(fn(input))
      }
    }
    fsaFn._t = type
    return fsaFn
  }

  function createSimpleAction<Constraint: string>(
    t: Constraint
  ): ActionCreator<Constraint, void, void> {
    const type = prefix + t
    const fsaFn: any = () => ({ type })
    fsaFn._t = type
    return fsaFn
  }

  return { createAction, createSimpleAction, createPromiseAction }
}

function createReducer<State>(initialState: State): Reducer<State> {
  let freezed = false
  const map = new Map()

  const reducer: any = (state: any = initialState, action: any) => {
    const handler = map.get(action.type)
    return handler ? handler(state, action.payload) : state
  }

  reducer.case = (actionFunc, callback) => {
    map.set(actionFunc._t, callback)
    return reducer
  }
  return reducer
}
```


[flow playground](https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoUYCmBnGBLAOwBcBaAEz2wEMAjGTZNIgTwAcGBBAYyLzgIDCAJ0xUicIQB4B-bESFVCRAFxg5QwgHMANGACSBVgFciugApVm8KmQB8YALxgA3qjBgAFAeNEAlKucwFnZVGQJ1RWJdVktrMlULKzgbMABfVHTUYIYAJUwyIy5MKQBlIjFMeydXdw8yit0qAmZ-MHqiTG03ME1MFU9fR3s8gqLS8o7bLvcuKmxMaVl5SNN9QxNzWOS7D273bl5+YVFxKTCIpV1vDbBEuKm9z3bO262bQYd7Z+7WkcLiyTPWwZVCoKBGAg8PgEMA0Ix4GBkA7Q45iCQeOCsIjYAJgVgiKB4AAeAH5VOotGlBjUwFwlniCcTHGBMdiAHT4zCEolgAA+vLAAHJBaD3ODIYcYVwRBVkfxFuFlkpyfItFd1qs7ttbLt3O5+uclVFHlACKprkQhq8kjYfqo5YIZacFRcomsfJsbXYXI86YqgmwGE5OdywABqIK++lQagAMTNYCazGZHkIPnNGo+9hperAIiIRiEMJzuYD7GmpfcMS9qlNYAAZPWwKbU5nHu50rnO3qY1R42yAPqWpzZR75wsw3vx7qZMUQqH8WlOzBmIRwAC2lEwDpdRv6FIIOndNy1Nh1jwNSwUlxNCYtVtXG63klP3oFr7tYAdqOdhuvbotT17h9XM-TkMsgwZLkmQjIgo39KcEyTFM0xMDMfCzEDS3HIssMrbIK0ras4gSNdN3mNkRGwOAYAAN0wDwW1QvxfHbNJHm7MU4wIQdhwgsc+gnZtuJnUVm3nSUlxOTASjwddWHoHc-xWFUNEPc9c0vRV-zg9xWm-J0JF3HTdFouA8DIUzzO9EswMtbJmRDGDI1A6NuNUZCnA8TCPECBzUlY3NEN45lR1zHDJxEjsxIilwpNlBcCF0aVpNk+TFMS5Ll0fcjt0S9jMnFfKUoqP4xkBCZKlbPBeCoGBnlUZ5fnyf5xgqbNunoS0oBETAAC98mZKA6vmbo7LAdcqFYZkCEwJAAFkpu8sTxpEUZig85oUzkCpNuTJxCBqvA6ueRpEr2zDbPpAALJoyHoIRmUm1g2V6IgPCoRK2WyQK9Vi26CHu4owBJMAAaBoQPB2jozslDk3jIQYVQqUTujW1q2VmeYU0+yVY3nZK6pgGhPoAa0ux5nrZeZ3tx6F8chXjCZgYmyd+9xYvRsZRI5wTcK54oQXQYAwCMahelBcaaRKjoHQrGWZLkhS8sleXsrIrcHQyZk4QRJFEp-dEaScolVEFOkIQ6IRgBFALJfpKi+mZBW0uVh0PEFR2iEFVjxsILhneXd3BX9wVdA8Wi6tUAgjHXGhikwyOYF9+kyEwAOnAV4O064MPPCT6PY-joRE7qlP-X9jhsGYSFA+knLNcSj3-ZIOYa9z8OC7AGO44Tq0aVihuKKomj6IjsuMlYrJAzaSrmUCJOjEwQve8e9I-YII6Trn6owEX5ewAABnY1a69Klqxmq2r6sq37MbmBj-fD6GXmI7YKdc-1WFUylgwRsBgAi0PAJAsuESzuH3sjDobJ97hjxAjDiM475YwYjnZ+lVogIw-n9PmxY2KQLUJVGBdUl5gBIPAr0iCOy-UAXmTAABHeEIg6EFCJCQfET55gkE3GQIGCAqAiG6PfeYrYuBV3bughoFC4jYJmA7PoK9i6OQQZ-cC+YABMP9DzKK9AAoBmgQFCUOtfb41ChEoI8F7SRMNpHv37vBNRCju5F2Bn-XRtDgHhVwWAYxx0b4ozMagVafRj5OEhgvEhB9j6pF0P7DwABmXw5dHFEAAIzMkhvmQ+ugvbeWSZaDRGTLF9FSboHOHh1FJKCbIGimA2TwE0MUoglTUBAA)

で試せる

## TODO:

- npm の ライブラリとして切り出す
- TypeScript版を書く
- 非同期周りやThunk周りを Type Safe に扱える方法がないか考える
- 異常系の扱い