redux-sagaでズンドコキヨシ


若干乗り遅れた感がありますが、[redux-saga](https://github.com/yelouafi/redux-saga)でズンドコキヨシをやりました。

![Screen Shot 0028-03-31 at 14.02.15.png](https://qiita-image-store.s3.amazonaws.com/0/69860/3b3a52e6-1fe8-dfeb-5edd-d7855195c283.png)

`redux-saga` というのは `redux` で非同期処理を扱うためのMiddlewareで、平たく言うと[koa](https://github.com/koajs/koa)みたいにGenerator関数を使って非同期処理を同期処理のようなスタイルで書き下せます。思いつきの遊び半分で始めたズンドコキヨシが、正直ここまで `redux-saga` の本質に迫るサンプルになるとは予想もしていませんでした。`redux-saga` 自体の詳しいは後日別の記事で解説するとして、`redux-saga` が秘めているパワーを少しでも感じ取ってもらえたらと思います。

### [デモはこちら](http://kuy.github.io/kiyoshi/)

このデモには[redux-devtools](https://github.com/fcomb/redux-logger)が組み込まれています。右側の黒っぽいドックがそれです。送り出されたActionのログ表示、 **Reset** で初期状態に戻す、 **Commit** で現在の状態をスナップショットして、 **Revert** でいつでもスナップショット時点に戻れます。こちらも同様にお楽しみ下さい。尚、表示されていない場合は `Ctrl-H` で表示・非表示の切り替え、`Ctrl-Q` でドックの表示場所を変更できます。

## 元ネタ

<blockquote class="twitter-tweet" data-lang="en"><p lang="ja" dir="ltr">Javaの講義、試験が「自作関数を作り記述しなさい」って問題だったから<br>「ズン」「ドコ」のいずれかをランダムで出力し続けて「ズン」「ズン」「ズン」「ズン」「ドコ」の配列が出たら「キ・ヨ・シ！」って出力した後終了って関数作ったら満点で単位貰ってた</p>&mdash; てくも (@kumiromilk) <a href="https://twitter.com/kumiromilk/status/707437861881180160">March 9, 2016</a></blockquote>

すばらしいネタをありがとう。

## 実装

`redux` はファイル数が多いので抜粋を掲載し、動作するサンプル全体は下記リポジトリを参照してください。

[kuy/kiyoshi](https://github.com/kuy/kiyoshi)

```sagas.js
import { fork, take, put, call, select } from 'redux-saga/effects';
import {
  ZUN, DOKO, zun, doko, kiyoshi
} from './actions';

function wait() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, 750);
  });
}

function sing() {
  return 0.5 < Math.random() ? zun() : doko();
}

function* river() {
  while (yield select(state => state.app.music)) {
    yield put(sing());
    yield call(wait);
  }
}

function* expect(type) {
  const action = yield take('*');
  if (type !== action.type) {
    throw 'unexpected';
  }
}

function* check() {
  try {
    yield call(expect, ZUN);
    yield call(expect, ZUN);
    yield call(expect, ZUN);
    yield call(expect, ZUN);
    yield call(expect, DOKO);
  } catch (e) {
    return;
  }
  yield put(kiyoshi());
}

function* checkLoop() {
  while (true) {
    yield fork(check);
    yield take('*');
  }
}

export default function* rootSaga() {
  yield fork(river);
  yield fork(checkLoop);
}
```

## 解説

### Sagaの起動

わざわざ `redux-saga` を使ってズンドコキヨシするのですから、それを最大限に生かさない手はありません。というわけでズンドコキヨシの処理を2つに分けて考えてみます。

+ ズンドコ川の生成
+ キヨシチェック

これら2つの処理はそれぞれ `river` と `checkLoop` という2つのSagaに対応しています。一番下にある `rootSaga` は `fork` Effectでこの2つのSagaを起動します。どちらも `fork` なので起動したSagaの終了を待つことなく終了します。

### ズンドコ川の生成

次に **ズンドコ川** を生成する `river ` Sagaを見てみましょう。以下のような順番で処理が進みます。

1. ズンドコ川の終了チェック
2. ランダムに「ズン」と「ドコ」を出力
3. 一定時間待つ

ここでReducerの一部を提示します。

```reducers.js
const handlers = {
  app: {
    [ZUN]: (state, action) => {
      return { ...state, list: [ ...state.list, 'ズン' ] };
    },
    [DOKO]: (state, action) => {
      return { ...state, list: [ ...state.list, 'ドコ' ] };
    },
    [KIYOSHI]: (state, action) => {
      return { list: [ ...state.list, 'キ・ヨ・シ！' ], music: false };
    }
  }
};
```

ご覧のように `KIYOSHI` Actionが来たときは「キ・ヨ・シ！」を `list` に追加するだけでなく、`music` を `false` に変更しています。つまり、音楽が止まるとズンドコ川が終了するわけですね。

そして `sing` 関数によってランダムに「ズン」と「ドコ」のActionが生成されて `put` EffectによってStoreに送られます。

最後に、Promiseの説明でよく例として出てくるアレ（一定時間が経過したら `resolve` してくれるやつ）を使って一時停止してから `while` ループで最初に戻ります。

### キヨシチェック

さて、ここが `redux-saga` を使ったズンドコキヨシのキモになります。
`redux-saga` の `take` Effectは特定のActionが来るまで停止していてくれるので、これを使ったら[FSM](https://www.wikiwand.com/ja/%E6%9C%89%E9%99%90%E3%82%AA%E3%83%BC%E3%83%88%E3%83%9E%E3%83%88%E3%83%B3)っぽいもの作れるんじゃね？と思ったのが基本的なアイデアです。

#### キヨシチェック #1

これが最初に思いついた実装。

```
function* check() {
  yield take(ZUN);
  yield take(ZUN);
  yield take(ZUN);
  yield take(ZUN);
  yield take(DOKO);
  yield put(kiyoshi());
}
```

ただ、これだと順序関係なく4回 `ZUN` Actionが呼ばれて、1回 `DOKO` Actionが呼ばれると発動してしまいます。なぜかというと `take` Effectは指定したActionが来るまでは他のActionを無視して待つからです。意図しない動作ですね。

#### キヨシチェック #2

というわけでシーケンスの連続性をチェックしないとなりません。Actionを指定するかわりに `'*'` を使ってすべてのActionを監視します。もしパターンから外れた場合はすぐに `return` してこのSagaを終了します。ただ、Sagaが終了してしまうと1回しかリトライできない死にゲーになってしまいキヨシが可哀想です。そこで `checkLoop` Sagaでラップすることで無限にリトライできるようにします。それはそれでキヨシ可哀想ですが・・・。

```
function* check() {
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== DOKO) { return; }
}

function* checkLoop() {
  while (true) {
    yield call(check);
  }
}
```

さて、こちら一見うまく動きそうに見えますが、重大な問題があります。
「ズン」「ズン」「ズン」「ズン」「ズン」「ドコ」というシーケンスでは発動してくれないんです。「ドコ」「ズン」「ズン」「ズン」「ズン」「ドコ」でないと発動しません。

これは4回目の「ズン」が来た後、「ドコ」の出現を期待しますが、もしそうでなかった場合は状態は `return` によりリセットされて、最初からやり直しになってしまうからです。これもまた意図しない動作ですね。

#### キヨシチェック #3

リセット直後（または初期状態）からのチェックだと5回連続して「ズン」が来るパターンに対応できないことがわかりました。つまり、リセット直後の1つあと、さらにもう1つあと、・・・というように並行してチェックする必要があります。これはFSMを複数走らせることになり、タイミングとしてはActionが来るたびにそれを起点としたチェックを走らせるイメージです。これでどこに希望するシーケンスが現れても発動してくれますね。

ここが `redux-saga` の強みで、わずかな修正で終わります。`check` Sagaを並行して動かすために `call` Effectのかわりに `fork` Efectを使います。終了を待たないのですぐに戻ってきます。新しい `check` Sagaを起動するタイミングは新しいActionが来たときなので、戻り値は使いませんが `take` Effectを使って待たせます。

```
function* check() {
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== ZUN) { return; }
  let action = yield take('*');
  if (action.type !== DOKO) { return; }
}

function* checkLoop() {
  while (true) {
    yield fork(check);
    yield take('*');
  }
}
```

これをベースにして、ヘルパー関数などを用意して理想的な見た目のコードに整えます。

## 今後の課題

+ リズム感がよくない：「ドコ」と「キ・ヨ・シ！」の間にウェイトが必要
+ そもそも「キ・ヨ・シ！」を一文字ずつ出力できないものか・・・
+ `expect` 関数で投げた例外をキャッチしてるのにコンソールに例外が漏れてる・・・
+ `redux-saga` の利点はテストしやすいことです。テストないので書きます。