Promiseを使った逐次処理でユーザー入力との待ち合わせができるイベントループを記述する


## 何がしたいか

- ゲームループがループごとにイベントを複数生成するのだが、それを時系列に沿って順番に捌きたい
- でもオブザーバでグローバルなイベント投げまくるとすぐクソコード化する
- どうにかしてピュアなモデルからビューとの待ち合わせ(オブザーバー含む)を切り離したい。なんでもしますから！


というわけでちゃんと設計練って書いてみたら結構いいんじゃねとなったので、解説書く。

## 設計の概要

- `EventSource#createEvents()` は Eventの配列を生成する
  - 実際にはここがユーザーが記述する部分になる
- `EventRunner` はループ毎に`EventSource#createEvents()`を呼び、上から順に処理する
  - event.eventType毎に処理の仕方を記述する
  - この例ではEvent間の時間を100msとなるよ]うにしてるが、実際なんでもいい。

## コード

CoffeeScriptで書く。暗黙のリターンがPromiseと相性が良い。詳しくはコードで解説する。
今回はPromiseにはbluebirdを使ってるが、Promiseの規格を満たしているならなんでもいい。nodeでもブラウザでも動く。


```coffee
Promise = require 'bluebird'

class Event
  constructor: ({@eventType, @log}) ->

class EventSource
  createEvents: ->
    for i in [1..3]
      seed = Math.random()
      if 0 < seed < 0.5
        new Event
          eventType: 'a'
          log: 'eventType A'
      else if 0 < seed < 0.9
        new Event
          eventType: 'b'
          log: 'eventType B'
      else
        new Event
          eventType: 'waitInput'
          log: 'waitInput'

class EventRunner
  constructor: ->
    @source = new EventSource

  start: =>
    do update = =>
      @source.createEvents()
        .reduce @processEvent, Promise.resolve()
        .then update

  processEvent: (p, event) => new Promise (done) => p.then =>
    setTimeout (=>
      switch event.eventType
        when 'a'
          @log event.log
          done()
        when 'b'
          @log event.log
          done()
        when 'waitInput'
          @waitInput().then =>
            done()
        else
          throw 'unknown event type:'+event.eventType
    ), 100

  waitInput: -> new Promise (done) =>
    @log 'wait user input'
    # catch something event for your application
    setTimeout done, 1000

  log: (message) ->
    console.log 'log:', message

runner = new EventRunner
runner.start()
```

実行するとこうなる

```
~/s/event-runner (master) $ coffee main.coffee 
log: eventType B
log: eventType B
log: wait user input
log: wait user input
log: eventType A
log: eventType B
log: eventType B
log: eventType B
log: eventType B
log: eventType A
log: eventType A
log: eventType A
log: eventType A
log: eventType B
log: eventType A
log: eventType A
log: eventType A
log: wait user input
```

ログだとわかりにくいけど、それぞれの実行は前のイベントを待って実行されてる。

## 解説

EventSourceはEventのArrayを返せばなんでもいい。この例では5割でa, 4割でb, 1割でwaitInputを返す。適当に作ったんでなんでもいい。

```coffee
class EventSource
  createEvents: ->
    for i in [1..3]
      seed = Math.random()
      if 0 < seed < 0.5
        new Event
          eventType: 'a'
          log: 'eventType A'
      else if 0 < seed < 0.9
        new Event
          eventType: 'b'
          log: 'eventType B'
      else
        new Event
          eventType: 'waitInput'
          log: 'waitInput'
```

ループ部分

```coffee
  start: =>
    do update = =>
      @source.createEvents()
        .reduce @processEvent, Promise.resolve()
        .then update
```

eventの配列をreduceでPromiseチェーンを作り、全てのpromiseが吐けたら再帰する。詳しくは次の関数。

```coffee
  processEvent: (p, event) => new Promise (done) => p.then =>
    setTimeout (=>
      switch event.eventType
        when 'a'
          @log event.log
          done()
        when 'b'
          @log event.log
          done()
        when 'waitInput'
          @waitInput().then =>
            done()
        else
          throw 'unknow event type:'+event.eventType
    ), 100

```

具体的にイベント事にどう捌くか記述しているのだが、冒頭部分が肝で、

```coffee
  processEvent: (p, event) => new Promise (done) => p.then =>
```

processEventは「Promise化された」関数で(coffeeの暗黙のリターン)、第一引数のpはreduceで集約された前回のpromiseインスタンス。これをthenでチェーンしてやることで、シーケンシャルに実行するPromiseチェーンが作れる。


waitInputもPromise化された関数で、今回は実は関数名が嘘で、1秒待ってdoneを解決するだけ

```coffee
  waitInput: -> new Promise (done) =>
    @log 'wait user input'
    # catch something event for your application
    setTimeout done, 1000
```

たとえばボタン押したらこのdoneを解決するように書けば、ユーザーの入力を待って処理を再開することができる。jQueryを使う例を示す。

```coffee
# 再開するボタンを作る
$resumeButton = $('button.resume')
$resumeButton.hide() # 最初は表示しない

# ... 中略
  waitInput: -> new Promise (done) =>
    $resumeButton.show() # ユーザーにボタンを見せる
    $resumeButton.on 'click', => # click時のコールバックを定義
      $resumeButton.off() # clickが二重定義にならないように剥がす
      $resumeButton.hide() # 再度隠す
      done() # Promiseを解決する
```

これがユーザーとのインタラクションを定義する方法になる。


なんか昔仕事でみたコードがこんな感じのループ処理が地獄だった記憶があったんだけど、これぐらいだと綺麗にかけて良いのではないか！