Reduxで副作用を分離して状態を不変に扱うredux-elm-middlewareを使う


Reduxを採用しているアプリケーション向けの記事です。

Reduxの副作用処理で使われるMiddlewareといえば、公式サンプルでは[redux-thunk](https://github.com/gaearon/redux-thunk)が使われていて、[redux-saga](https://github.com/redux-saga/redux-saga)もよく使われているという印象があります。

redux-thunkよりredux-sagaが選ばれる理由の１つは、副作用処理を分離でき、ActionCreatorを純粋関数（副作用のない関数）に保てるところかと思います。

redux-thunkでは[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-thunk-flow-ducks/src/modules/count.js#L35-L36)ActionCreatorになり、redux-sagaだと[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-saga-flow-ducks/src/modules/count.js#L36)ActionCreatorになり、加えて[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-saga-flow-ducks/src/modules/count.js#L42-L49)Sagaを作ります。

redux-thunkでテストを書くとActionCreatorは[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-thunk-flow-ducks/src/modules/count.spec.js#L43-L49)非同期処理をモックしたコードになり、mapDispatchToPropsは[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-thunk-flow-ducks/src/containers/Counter.spec.js#L30-L37)コードになります。
redux-sagaだとActionCreatorは[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-saga-flow-ducks/src/modules/count.spec.js#L46-L48)コードになり、Sagaは[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-saga-flow-ducks/src/modules/count.spec.js#L55-L65)コードになり、mapDispatchToPropsは[こういう](https://github.com/boiyaa/counter-js/blob/master/redux-thunk-flow-ducks/src/containers/Counter.spec.js#L30-L37)コードになります。

このように副作用処理を分離するMiddlewareは他にも色々ありますが、[redux-elm-middleware](https://github.com/stoeffel/redux-elm-middleware)という、AltJSの[Elm](http://elm-lang.org/)を利用するMiddlewareがあったので、試しに使ってみました。


# Elmとは
- 型安全、Null安全によりランタイムエラーが無い言語
- すべての型が不変値
- 副作用はElmランタイムが処理し、コードはすべて純粋関数
- ハイパフォーマンスなJavaScriptを生成する
- 採用事例増加中

> A small but noticeable number of developers are starting to choose Elm over JavaScript.
[Front-End Developer Handbook 2017](https://frontendmasters.com/books/front-end-handbook/2017/recap.html)

- そんな難しくない文法（[ElmとJavaScriptのシンタックス比較表](http://qiita.com/boiyaa/items/7050eb93106a93a8dfb8)）


# redux-elm-middlewareとは
JSからElmにActionを渡し、Elmで更新されたStateをJSで受け取るMiddlewareと、そのStateを適用するReducerを提供します。

元々ReduxはElmをインスパイアして作られたので、状態が不変であるとか、Reducerが純粋関数であるとかのRedux原則はElmの言語仕様だったりするので、相性はそんなに悪くないです。が、正直無理して組み合わせるよりはElm単独で使った方がいいかなとは思います。

とはいえ、Elmで1から作るのは新規開発でもないとなかなかハードルが高いので、このMiddlewareで部分的に既存の処理を置き換えるというのならありじゃないかなと思っています。


# Counterを実装してみる
※この先Elmコードがそこそこ出てきますが、あまり細かい説明をしていませんので、完璧に読めないと気が済まない方は先に記事の一番下のリンクをご参照ください。

[こちら](https://github.com/boiyaa/counter-js/tree/master/redux-saga-flow-ducks)のsagaで作ったCounterサンプルをベースにします。

[こちら](https://guide.elm-lang.org/install.html)を参考にelmをインストールします。

elm-loaderを追加します。

```sh
$ yarn add -D elm-webpack-loader
```

redux-elm-middlewareを追加します。

```sh
$ yarn add redux-elm-middleware
```

Counter.jsのmapDispatchToPropsを変更します。
ActionCreatorを介すことは無くなるので、直接Actionをdispatchします。

```js:containers/Counter.js
export const mapDispatchToProps = (dispatch: Dispatch) => ({
  onIncrease: () => {
    dispatch({ type: "INCREASE" })
  },
  onIncreaseIfOdd: () => {
    dispatch({ type: "INCREASE_IF_ODD" })
  },
  onIncreaseAsync: () => {
    dispatch({ type: "INCREASE_ASYNC" })
  },
  onDecrease: () => {
    dispatch({ type: "DECREASE" })
  }
})
```

次に、[modules/count.js](https://github.com/boiyaa/counter-js/blob/master/redux-saga-flow-ducks/src/modules/count.js)でやっている処理をElmに移植します。

ElmにはPortsという、JSと値をやり取りするためのPubSubの仕組みがあり、
redux-elm-middlewareでは、dispatchされてきたActionTypeと同名のElmのPortにActionを送信し、更新された値を受信します。
同名と書きましたが、正確にはcamelCaseに変換した文字列になります。
`{ type: "INCREASE" }`であれば、以下のようにPortを定義します。

```hs:modules/Reducer.elm
port increase : (Value -> msg) -> Sub msg
```

ElmでのActionにあたるMsgを定義します。

```hs:modules/Reducer.elm
type Msg
    = Increase
```

値の変更を監視するsubscriptionsにて、increaseが値を受け取ったら、Increaseを発行する処理を定義します。

```hs:modules/Reducer.elm
subscriptions model =
    increase <| always Increase
```

ElmでのStateにあたるModelの型エイリアスを定義します。

```hs:modules/Reducer.elm
type alias Model =
    Int
```

ElmでのReducerにあたるupdateを定義します。

```hs:modules/Reducer.elm
update msg model =
    case msg of
        Increase ->
            ( model + 1, Cmd.none )
```

Elmアプリケーションの初期化関数initを定義します。

```hs:modules/Reducer.elm
init =
    ( 0, Cmd.none )
```

ModelをJSでの型に変換する関数encodeを定義します。

```hs:modules/Reducer.elm
encode model =
    int model
```

Elmランタイムにアプリケーションを登録します。

```hs:modules/Reducer.elm
main =
    Redux.program
        { init = init
        , update = update
        , encode = encode
        , subscriptions = subscriptions
        }
```

こうして作ったReducer.elmを、redux-elm-middlewareのcreateElmMiddlewareとcreateElmReducerを使ってJS側でReducerとして扱います。

```js:configureStore.js
// @flow
import { applyMiddleware, combineReducer, createStore } from "redux"
import createElmMiddleware, { createElmReducer } from "redux-elm-middleware"
import Elm from "./modules/Reducer.elm"

const initialState = 0
const countReducer = createElmReducer(initialState)
const rootReducer = combineReducers({
  count: countReducer
})

const { run, elmMiddleware } = createElmMiddleware(Elm.Reducer.worker())

const store = createStore(
  rootReducer,
  preloadedState,
  applyMiddleware(elmMiddleware)
)

run(store)
```

これが一通りの実装方法になります。
例えば`{ type: "INCREASE_ASYNC" }`のような非同期処理のあるActionの場合は、以下のようにModelとセットで非同期処理の関数を返すことで、ランタイム側で処理してくれます。

```
update msg model =
    case msg of
        Increase ->
            ( model + 1, Cmd.none )

        IncreaseAsync ->
            ( model, increaseInSecond )


increaseInSecond : Cmd Msg
increaseInSecond =
    setTimeout Increase 1000


setTimeout : Msg -> Float -> Cmd Msg
setTimeout msg delay =
    perform (always msg) (sleep (delay * millisecond))
```

テストは純粋関数ですのでシンプルになります。

```
suite =
    describe "Reducer Test Suite"
        [ describe "init"
            [ it "returns initial model" <|
                expect init to equal ( 0, Cmd.none )
            ]
        , describe "update"
            [ it "returns increased model" <|
                expect (update Increase 0) to equal ( 1, Cmd.none )
            , it "returns model and command that increases in a second" <|
                expect (update IncreaseAsync 1) to equal ( 1, increaseInSecond )
            ]
        ]
```

こんな要領で実装したCounterを[こちら](https://github.com/boiyaa/counter-js/tree/master/redux-elm-flow-ducks)に置いてありますので、興味があればごらんください。

また、もう少し本格的なアプリケーション開発に使えそうなボイラープレートも作っていますので、よかったらこちらもみてみてください。

[react-redux-elm-boilerplate](https://github.com/boiyaa/react-redux-elm-boilerplate)


# もっとElmを知りたいと思ったら
この辺りを見るといいかもしれません。

- [Ellie](https://ellie-app.com/) - オンラインREPL
- [Elm Examples](http://elm-lang.org/examples)
- [Get Started](https://guide.elm-lang.org/install.html)
- [Elm Tutorial](https://www.elm-tutorial.org/) - 日本語あります