読者です 読者をやめる 読者になる 読者になる

VRゴーグルを作ってみた

大晦日

何してましたか?僕は連日飲みすぎて気分悪いので、引きこもって工作してました。

VR!VR!

VR流行ってますよね、たぶん。気になってます。

Oculus RiftとかPSVRとかいろいろあると思うんですが、少々お高い!

合わない人は合わないらしいんで、買って後悔するのもあれだなーとか思いながら、都内でVR体験できるとこあるけど行くのめんどいなーとか。

そしたらこんなんあるんですね。

ダンボール製やん → 自作できるよね?ってことで調べてみたら簡単に作れるんですねー。

というわけで数十年ぶりに工作してみました。ハマったところだけ強調しときます。

いざ100均

ダイソーでミニルーペをゲッツします。

f:id:ktdk:20161231120452j:plain

売り場

老眼鏡とかサングラスの横 (10分くらいハマりました)

取り外し方

ひねれば取れるとかあったんですが、たぶん最近のやつは接着剤でがっちりくっついてるっぽいんですね。

まずマイナスドライバーでテコの原理を利用して分解します。

更にマイナスドライバーでテコの原理を利用して1枚目のレンズを分離します

そして2枚目のレンズは周りをニッパーなどでバッキバキにし、マイナスドライバーでテコの原理を利用してとり外します。

f:id:ktdk:20161231121649j:plain

箱作り

お菓子の箱でもなんでもいいんですが、懐かしい工作用紙が売ってたのでそれで作ることにしました。

f:id:ktdk:20161231124620j:plain

で、レンズ間の距離が重要ですのでこれだけはしっかりと調整します。

VR対応のアプリとか表示しながら、レンズどうしを近づけたり目やスマホとレンズの距離を調整してベストな間隔を決めます。

ちなみに僕の場合はレンズ間が3.5cm、スマホとレンズの距離が5cmくらいでいい感じに見えました。

組み立て

だいぶ雑いですが完成しました!

f:id:ktdk:20161231125949j:plain

いざVR!

VRの元動画とか見たときは何やただの立体視だかマジカル・アイみたいなもんか、思ってたんですが...

こんなダンボールのくせに!溢れ出す臨場感!!

次は天空でVR楽しんでこようかと思います。

天空でVR(バーチャルリアリティ)体験

良いお年を!!

Elixir - Agent

Agent

Agent - Elixir

Agentは状態を保持します。

# 空のリストを初期状態として生成
{:ok, agent} = Agent.start_link(fn -> [] end)

# 現在の状態(list)を引数として新しい状態を生成する
Agent.update(agent, fn list -> ["eggs" | list] end)

# 現在の状態を取得する
Agent.get(agent, fn list -> list end)

# プロセスを終了する
Agent.stop(agent)

Process.alive?(agent)
# > false

Agentを利用してキーバリューストアを実装してみます。

# kv.exs
defmodule KV do
  def start_link do
    Agent.start_link(fn -> %{} end)
  end

  def get(kv, key) do
    Agent.get(kv, &Map.get(&1, key))
  end

  def put(kv, key, value) do
    Agent.put(kv, &Map.put(&1, key, value))
  end

  def delete(kv, key) do
    Agent.get_and_update(kv, &Map.pop(&1, key))
  end
end

iexから実行してみます。

$ iex kv.exs
iex> {:ok, kv} = KV.start_link
{:ok, #PID<0.64.0>}
iex> KV.put(kv, :hello, "world")
:ok
iex> KV.get(kv, :hello)
"world"
iex> KV.delete(kv, :hello)
"world"
iex> KV.get(kv, :hello)
nil

Process間でAgentを共有してみる

一つのカウンターをマルチプロセスでカウントアップしてみます。

# counter.exs
defmodule Counter do
  def start_link do
    Agent.start_link(fn -> 0 end)
  end

  def increment(counter) do
    Agent.update(counter, fn x -> x + 1 end)
  end

  def count(counter) do
    Agent.get(counter, fn x -> x end)
  end
end

defmodule CountProcess do
  def start_link(counter, parent) do
    {:ok, spawn_link(fn -> loop(counter, parent) end)}
  end

  defp loop(counter, parent) do
    count = Counter.count(counter)

    IO.puts "#{inspect(self)} #{count}"

    cond do
      count < 10_000 ->
        Counter.increment(counter)
        loop(counter, parent)
      true ->
        IO.puts "#{inspect(self)} #{count} Completed!!"
    end
  end
end

defmodule ConcurrentCounter do
  def run(n) do
    {:ok, counter} = Counter.start_link
    Enum.each(1..n, fn x -> CountProcess.start_link(counter, self) end)
  end
end
$ iex counter.exs
iex> ConcurrentCounter.run(4)
...
#PID<0.71.0> 9999
#PID<0.70.0> 9999
#PID<0.72.0> 10003
#PID<0.69.0> 10003
#PID<0.71.0> 10003
#PID<0.70.0> 10003
#PID<0.72.0> 10003 Completed!!
#PID<0.69.0> 10003 Completed!!
#PID<0.71.0> 10003 Completed!!
#PID<0.70.0> 10003 Completed!!

複数のプロセスが起動してカウントアップしているのが確認できました。
が、同期を取っていないので10,000を超えてカウントされてしまっています。

Javaだとスレッドでsynchronizedとかして同期させた記憶があるのですが、Elixirだとsend-receive? Task? GenServer? この辺りを利用するのかな?

Elixir - Process

Getting StartedrのProcessについてサクッとまとめました。

Processes - Elixir

  • spawn ... 子プロセスを生成する
  • spawn_link ... 子プロセスを生成する(例外発生時に共倒れ)
  • send, receive ... プロセス間のメッセージ送受信
  • Task ... spawnのラップ、結果も返してくれる
  • State ... send, receiveの再帰呼び出しで実現

Processes

すべてのコードはプロセス中で実行されます。プロセスはお互いに隔離されており、並列で実行されメッセージパッシングを通じてやり取りします。並列処理のベースであるだけではなく、これにより分散型のフォールトトレラントプログラムを構築する役割があります。

ElixirのプロセスはOSのそれと混同すべきではありません。他のプログラミング言語のスレッドとは違い、メモリとCPUの観点からすると非常に軽量です。そのため、数十万のプロセスを同時に実行することも珍しくありません。

spawn

spawn/1は新しいプロセスを生成しPIDを返します。 生成されたプロセスは関数を実行し終了します。

iex> pid = spawn fn -> 1 + 2 end
#PID<0.58.0>
iex> Process.alive?(pid)
false

プロセス自身のIDはself/0で取得できます。

iex> self()
#PID<0.56.0>
iex> Process.alive?(self())
true

send と receive

プロセス間のメッセージのやり取りにはsend/2receive/1を利用できます。 以下は自分自身にメッセージを送信するサンプルです。

defmodule ProcessSample do
  def send_message(msg) do
    # 自分自身のメールボックスに保存
    send self(), msg
  end

  def receive_message do
    # 送信されたメッセージを受け取る
    receive do
      {:msg, msg} -> msg
    after
      # タイムアウトを設定
      # ※設定しない場合はパターンマッチするメッセージを受信するまで待ち続ける
      1_000 -> 'timeout'
    end
  end
end

# 送信したメッセージがパターンマッチする場合
ProcessSample.send_message({:msg, 'hello, world'})

IO.puts ProcessSample.receive_message

# マッチしない場合
ProcessSample.send_message('hello, world')

IO.puts ProcessSample.receive_message
$ elixir process.exs
hello, world
timeout

これをspawn/1を利用してプロセス間でやり取りするように書き換えてみます。

defmodule InterProcessSample do
  def run do
    parent = self()

    # PIDを送信する子プロセスを生成
    spawn fn -> send(parent, {:pid, inspect(self())}) end
  end

  def receive_message do
    receive do
      {:pid, pid} -> "I'm #{pid}."
    after
      1_000 -> 'timeout'
    end
  end
end

InterProcessSample.run

IO.puts InterProcessSample.receive_message
$ elixir process.exs
I'm #PID<0.52.0>.

Links

プロセスの生成で実際によく利用されるのはspawn_link/1です。 spawn/1は子プロセスが例外を起こしても親プロセスは実行され続けますが、 spawn_link/1は親プロセスも共に終了します。

# spawn.exs

# spawn/1は子プロセスで例外が起きてもプログラムは終了しない
# spawn fn -> raise "oops" end

# spawn_link/1はプログラムが終了する
spawn_link fn -> raise "oops" end

receive do
  :hello -> '子プロセスが例外を起こすまで待つ'
end

この仕組みはフォールトトレラントシステムを構築するのに重要な役割を果たします。 Elixirではプロセスの死活監視、生成を行うSupervisorにリンクすることが多いです。

他のプログラミング言語ではtry/catch等で例外処理をしますが、 このような仕組みがあるため、例外は放っておくのが実際のところです。

Tasks

Taskはspawnより詳細なエラーレポートを提供します。 Task.start/1, Task.start_link/1はPIDと併せて結果も返してくれるためSupervisorで利用されます。

iex> spawn fn -> raise "oops" end
#PID<0.58.0>
iex>
10:40:03.817 [error] Process #PID<0.58.0> raised an exception
** (RuntimeError) oops
    :erlang.apply/2

iex> Task.start fn -> raise "oops" end
{:ok, #PID<0.60.0>}
iex>
10:40:11.695 [error] Task #PID<0.60.0> started from #PID<0.56.0> terminating
** (RuntimeError) oops
    (elixir) lib/task/supervised.ex:94: Task.Supervised.do_apply/2
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Function: #Function<20.54118792/0 in :erl_eval.expr/5>
    Args: []

State

アプリケーションの設定や読み込んだファイルの内容等の状態を保持する場合にもプロセスを利用します。
以下はキーバリューストアのサンプルです。

# kv.exs

defmodule KV do
  def start_link do
    # 空のMapとともに子プロセスを生成
    Task.start_link(fn -> loop(%{}) end)
  end

  defp loop(map) do
    receive do
      {:get, key, caller} ->
        # caller(呼び出し元)にメッセージを送信
        send caller, Map.get(map, key)
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end
end

これをiexで実行してみます。 flush/0はメールボックス内のメッセージを出力します。

$ iex kv.exs
iex> {:ok, pid} = KV.start_link
{:ok, #PID<0.62.0>}
iex> send pid, {:put, :hello, :world}
{:put, :hello, :world}
iex> send pid, {:get, :hello, self()}
{:get, :hello, #PID<0.60.0>}
iex> flush
:world
:ok

また、pidに名称を与えて実行することも可能です。

iex> {:ok, pid} = KV.start_link
{:ok, #PID<0.63.0>}
iex> Process.register(pid, :kv)
true
iex> send :kv, {:put, :hello, :world}
{:put, :hello, :world}

大抵の場合これらのパターンを自前で実装することはなく、 例えばAgent等の抽象化されたものを利用します。

iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.72.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world

Elixirのパターンマッチ

JSONを取得するサンプル

適当なAPIサーバーを立ててJSONを取得してみます。

APIサーバー

http://localhost:3000/capitals.jsonにアクセスすると首都一覧を返します。

const express = require('express')
const app = express()

app.get('/capitals.json', (request, response) => {
  response.contentType('application/json')

  const capitals = [
    { country: 'China', city: 'Beijing' },
    { country: 'Japan', city: 'Tokyo' },
    { country: 'Mongolia', city: 'Ulan Bator' },
    { country: 'North Korea', city: 'Pyhongyang' },
    { country: 'South Korea', city: 'Seoul' },
  ]

  response.send(JSON.stringify(capitals))
})

app.listen(3000)
クライアント

HTTPoisonのサンプル通りに記述してJSONを取得します。

defmodule JsonHandler do
  def main(args) do
    fetch_capitals
  end

  def fetch_capitals do
    url = "http://localhost:3000/capitals.json"

    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        IO.puts body
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        IO.puts "Not found :("
      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.puts "Unknown error"
    end
  end
end
実行
$ ./json_handler
[{"country":"China","city":"Beijing"},{"country":"Japan","city":"Tokyo"},{"country":"Mongolia","city":"Ulan Bator"},{"country":"North Korea","city":"Pyhongyang"},{"country":"South Korea","city":"Seoul"}]

最初のcaseにマッチするのでレスポンスが出力されました。

次にサーバーを起動しない状態で実行すると...

$ ./json_handler
Unknown error

最後のcaseにマッチするのでメッセージが出力されます。

ではマッチしないケースはどうなるでしょうか。
503を返すようにサーバーを書き換えて実行してみます。

response.status(503)
response.send()
$ ./json_handler
** (CaseClauseError) no case clause matching: {:ok, %HTTPoison.Response{body: "", headers: [{"X-Powered-By", "Express"}, {"Content-Type", "application/json"}, {"Date", "Thu, 18 Aug 2016 04:39:06 GMT"}, {"Connection", "keep-alive"}, {"Content-Length", "0"}], status_code: 503}}
    (error_handling_sample) lib/error_handling_sample.ex:9: ErrorHandlingSample.fetch_capitals/0
    (elixir) lib/kernel/cli.ex:76: anonymous fn/3 in Kernel.CLI.exec_fun/2

マッチするケースがないよ、ってことでクラッシュしてしまいます。
というわけで200, 404以外にマッチするパターンを追加します。

    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        IO.puts body
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        IO.puts "Not found :("
      {:ok, %HTTPoison.Response{status_code: status_code}} ->
        IO.puts "Status code #{status_code}"
      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.puts "Unknown error"
    end

これで503も拾えました。

$ ./json_handler
Status code 503

case文を関数化する

もう少しElixirらしく?、各パターンを関数に書き換えます。
要注意なのがcase文と同じく先にある関数からマッチがされていくという点です。
つまり関数の記述順序も気にしないといけません。(まぁこの場合はパターンが悪いと思いますが)

defmodule JsonHandler do
  def main(args) do
    fetch_capitals
  end

  def fetch_capitals do
    url = "http://localhost:3000/capitals.json"

    HTTPoison.get(url)
    |> process_response
  end

  defp process_response({:ok, %HTTPoison.Response{status_code: 200, body: body}}) do
    IO.puts body
  end

  defp process_response({:ok, %HTTPoison.Response{status_code: 404}}) do
    IO.puts "Not found :("
  end

  defp process_response({:ok, %HTTPoison.Response{status_code: status_code}}) do
    IO.puts "Status code #{status_code}"
  end

  defp process_response({:error, %HTTPoison.Error{reason: reason}}) do
    IO.puts "Unknown error"
  end
end

クラッシュ怖い

マッチするパターンがないとクラッシュするって言われると、あらゆるパターン(特にエラー処理)を想定しなければならず、正常系よりも異常系の処理を書くことがメインのお仕事になってきます。

そんなの辛い。

"Let it crash."

qiita.com

Elixir(とそのベースになっているErlang)のプロセスは生成のためのコストが小さいため「下手にエラー処理するコードを書いてプロセスを維持するよりはさっさとクラッシュさせて、それに続く処理の中で対策して再起動したほうがよい」という思想があります。

クラッシュ上等というわけですね。

というわけで今度はクラッシュしまくるスタイルに書き換えてみる予定です。

ElixirでWebスクレイピング

Webページの取得にHTTPoison, HTMLのパースにFlokiを利用します。

HTTPoison

GitHub - edgurgel/httpoison: Yet Another HTTP client for Elixir powered by hackney

Floki

GitHub - philss/floki: Floki is a simple HTML parser that enables search for nodes using CSS selectors.

実行環境を整える

プロジェクトの作成

$ mix new scraper_sample

関連モジュールの取得

mix.exsに依存関係を記述し取得します。

# mix.exs
defmodule NetseaCrawler.Mixfile do
  use Mix.Project

  # ....

  def application do
    [applications: [:logger, :floki, :httpoison]]
  end

  defp deps do
    [
      {:floki, "~> 0.10.0"},
      {:httpoison, "~> 0.9.0"},
    ]
  end
end
$ mix deps.get
Running dependency resolution
Dependency resolution completed
  certifi: 0.4.0
  floki: 0.10.0
  hackney: 1.6.1
...

iexの起動

以下のコマンドでiexを起動するとプロジェクトの環境が読み込まれるので、依存モジュールを参照することができます。

$ iex -S mix

スクレイピングしてみる

試しにelixirのページのコミットメッセージを取得してみます。

GitHub - elixir-lang/elixir: Elixir is a dynamic, functional language designed for building scalable and maintainable applications

HTTPoisonでWebページを取得する

iex(1)> ret = HTTPoison.get! "https://github.com/elixir-lang/elixir"
iex(2)> %HTTPoison.Response{body: body} = ret

これで変数bodyにHTMLが入ります。

Flokiでパース

ブラウザの「要素の調査」とかでHTMLの構造を把握します。
コミットメッセージは table.files > td.message > span で取れそうなのでFloki.findをパイプライン演算子でつなげていきます。(XPathで書けなそうなのでネストが深いと結構面倒)

iex(3)> Floki.find(body, "table.files") |>
...(3)> Floki.find("td.message") |>
...(3)> Floki.find("span") |>
...(3)> Enum.map(fn(span) -> Floki.text(span) end)
["Make batch style more consistent (#4944)",
 "Refactor and optimize aggregations over lists",
 "Fix a typo in the iex man page (#4655)", "Start v1.4.0-dev",
 "Move .gitattributes files to repo root",
 "Include docs zip in the publish process", "Use 18.2 instead of 18.2.1",
 "Integer.digits/2 & Integer.undigits changes (#4868)",
 "Small fixes in comments and documentation (#4744)",
 "Add Elixir Forum to list of help channels (#5121)",
 "Copyright requires just the starting year",
 "Properly check for earlier Erlang versions, closes#5090",
 "Add missing NOTICE file", "Add link to security mailing list",
 "Start v1.4.0-dev", "Start v1.4.0-dev",
 "Update rebar to rebar 2.1.0-pre. This (among other things) fixes a cr…",
 "Use proper case for proper nouns and acronyms", "Rebar3 dependency support"]

エラー処理とか全くしてない状態なので、今度はその辺突っ込んでみようかと。

プログラミングElixir

プログラミングElixir

YQLを利用してRSSを取得する

はてブのホットエントリーをJSON形式で取得してReduxを利用したクライアントで表示してみます。

完成品

f:id:ktdk:20160802104044g:plain

(左に寄っちゃってますが...)

YQL (Yahoo! Query Console)

CORSによるクロスドメイン制限があるため、Ajaxで直接RSSを取得することはできません。
PHP等のサーバーサイドでRSSを取得してクライアントに表示する必要があります。

今回はサーバーの実装をしないので、YQLを利用します。

developer.yahoo.com

こんな感じでSQLのようにクエリを実行すると結果が返ってきます。

f:id:ktdk:20160802102122p:plain

最下部にあるTHE REST QUERYcurlで叩いてみると、RSSJSON形式で取得できます。

$ curl -X GET "https://query.yahooapis.com/v1/public/yql?q=SELECT%20*%20FROM%20rss%20WHERE%20url%20%3D%20'http%3A%2F%2Fb.hatena.ne.jp%2Fhotentry.rss'&format=json&diagnostics=true&callback="

{"query":{"count":30,"created":"2016-08-02T01:23:13Z","lang":"en-US","diagnostics":{"publiclyCallable":"true","url":{"execution-start-time":"1","execution-stop-time":"452","execution-time":"451","content":"http://b.hatena.ne.jp/hotentry.rss"},"user-time":"454","service-time":"451","build-version":"0.2.39"},"results":{"item":[{"about":"http://www.okinawatimes.co.jp/articles/-/55298","title":"スク水揚げ 奥武島で今年も大漁 | 沖縄タイムス+プラス ニュース","link":"http://www.okinawatimes.co.jp/articles/-/55298","description":"【南城】南城市の奥武島でスク漁が始まり、島の海人が
...

Fetch API

RSSを取得するだけなのでsuperagent等のモジュールではなくFetch APIを利用してみます。

Fetch Standard (日本語訳)

redux-actions、redux-promiseと組み合わせる

fetch()はPromiseを返すのでredux-promiseにそのまま渡します。
then()でreturnした結果がreducerのnextのaction.payloadに渡されます。

// api/bookmarks.js
import querystring from 'querystring'
import { getRssUrl } from '../constants/ApiCategory'

const YQL_URL = "https://query.yahooapis.com/v1/public/yql"

const defaultParams = {
  format: 'json',
  diagnotistics: true
}

export default {
  fetchBookmarks: (category) => {
    const sql = `SELECT * FROM rss WHERE url = '${getRssUrl(category)}'`
    const data = Object.assign({}, defaultParams, { q: sql })
    const url = `${YQL_URL}?${querystring.stringify(data)}`

    // Promiseを返す
    return fetch(url)
      .then((res) => {
        return res.json()
      })
      .catch((err) => {
        console.log(err)
      })
  }
}
// actions/api.js
import { createAction } from 'redux-actions'
import * as types from '../constants/ApiActions'
import api from '../api/bookmarks'

export const fetchBookmarks =
  createAction(types.FETCH_BOOKMARKS, api.fetchBookmarks)
// reducers/bookmarks.js
const bookmarks = handleActions({
  [types.FETCH_BOOKMARKS]: {
    next: (state, action) => {
      let items = action.payload.query.results.item
      return items.map(b => bookmark(b, action))
    },

    throw: (state, action) => {
      return []
    }
  }
}, [])

今回は1つのstateを全タブで共有しているためクリック毎にrequestが飛んでしまいますが、
カテゴリ(タブ)ごとにstate持たせて管理するとrequest回数も減らせてサクサク動きますかね。

ソースコード

github.com

How to build Rails5 API + Redux ToDo Application その4

前回はクライアントをRedux化(API叩かない版)しました。

kogoto.hatenablog.com

これにAPIを叩かせて完成とします。

github.com

CORS

実装前に、クロスドメインの問題があるので、Railsのほうでごにょごにょします。

# config/application.rb
module Rails5ReactReduxTodo
  class Application < Rails::Application

    # ...

    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*'
        resource '*', :headers => :any, :methods => [:get, :post, :patch, :delete]
      end
    end
  end
end

SuperAgent

AJAXしたいだけなのでjQueryではなくHTTPリクエストに特化したライブラリを利用します。

github.com

$ npm install --save superagent

まず、Railsサーバーを起動してREPLからAPIを叩いてみます。

サーバーを起動

$ rails s

REPLから実行

$ node

get: ToDO一覧を取得

> request = require('superagent')
> request.get('http://localhost:3000/todos').end(function(err, res) { console.log(res.body) })
> [ { id: 1,
    title: '単2電池を買いに行く',
    completed: false,
    order: 1,
    created_at: '2016-07-29T00:20:23.797Z',
    updated_at: '2016-07-29T00:20:23.797Z' } ]

post: ToDOを登録

> request.post('http://localhost:3000/todos').send({ todo: { title: 'ストレッチ', order: 1 }}).end(function(err, res) { console.log(res.body) })
> { id: 2,
  title: 'ストレッチ',
  completed: null,
  order: 1,
  created_at: '2016-07-29T00:23:17.983Z',
  updated_at: '2016-07-29T00:23:17.983Z' }

patch: ToDOを完了状態に更新

> request.patch('http://localhost:3000/todos/2').send({ todo: { completed: true }}).end(function(err, res) { console.log(res.body) })
> { id: 2,
  completed: true,
  title: 'ストレッチ',
  order: 1,
  created_at: '2016-07-29T00:23:17.983Z',
  updated_at: '2016-07-29T00:25:39.456Z' }

delete: ToDOを削除

> request.del('http://localhost:3000/todos/2').end(function(err, res) { console.log(res.body) })
> {}

こんな感じでWeb APIにアクセスできます。

redux-promise

redux-promiseはcreateActionにPromiseを渡すとよしなにしてくれるMiddlewareです。
redux-actionsと併せて使います。

まずはPromiseを返すWeb APIを作成します。
以下はToDO一覧を取得するAPIの場合です。

// api/todos.js
export default {
  fetchTodos: () => {
    return new Promise((resolve, reject) => {
      request
        .get(url)
        .end((err, res) => {
          if (!res || res.status === 404) {
            reject()
          } else {
            resolve(res.body)
          }
        })
    })
  },
  /* ... */
}

Action作成時にこの関数を渡します。

// actions/api.js
export const fetchTodos = createAction(ApiAction.FETCH_TODOS, api.fetchTodos)

Actionのハンドリングをします。
nextのaction.payloadにはresolveの引数であるbody(todos)が格納されています。

// reducers/todos.js
const todos = handleActions({
  [ApiAction.FETCH_TODOS]: {
    next: (state, action) => action.payload,
    throw: (state, aciton) => []
  },
  /* ... */
}, [])

このActionをcomponentDidMount時に呼び出します。

// App.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import * as actionCreators from './actions/api'

export default class App extends Component {
  componentDidMount() {
    this.handleFetchTodos()
  }

  handleFetchTodos() {
    this.props.fetchTodos()
  }
  /* ... */
}

App = connect(null, actionCreators)(App)

export default App

これを実行しredux-loggerを利用してActionのログを見てみます。

f:id:ktdk:20160730125821p:plain

自前でログを仕込んでみたところrequest, responseの間に1回目のFETCH_TODOSが発行されています。
(おそらくこの部分はredux-promiseが処理してくれている)

で、resolveのタイミングで2回目のFETCH_TODOSが発行されhandleActionsのnextがコールされます。

これで非同期でAPIを叩いて動くToDOアプリケーションの完成です。

つぎは何作ろっかなー