みずぴー日記

人間の再起動ボタンはハワイのビーチにある

🎤発表資料

勉強会の発表資料を作る際に気をつけていること。 最近だと📝The reason for using reason #ML_study - みずぴー日記を作った。

関係ない話は削る

発表で言いたいことがぼやけてしまうので、余分な話はしないようにしている。

自己紹介をいれると発表のテーマと違う話をすることになってしまうので、省略している。だいたいは「こんにちは、mzpです」くらいで済ませてる。

話の流れを整理するために事前に発表原稿を作っている。 何度か読み直して余分な話を削っている。 発表後は、このときの原稿をブログに貼り付けて、記事にしている。

f:id:mzp:20170727160306p:plain

導入は丁寧にする

「なぜこの技術が必要なのか」「なぜこの技術が登場したのか」といった動機の説明が不十分だと魅力がうまく伝わらないので、ここに時間を割く。

原稿や資料を作る際もここに時間をかけている。

「背景」→「困っていること/欠点」→「解決策」というストーリーにすることが多い。

後ろの席からも見やすいスライドにする

会場の形状によっては、前の席の人の頭でスライドの下のほうが隠れてしまうことがある。なので、文字を大きくしつつ、スライドの下のほうには空にするか補助情報だけを書くようにしている。

f:id:mzp:20170728071321p:plain

色を減らす

会場の明さやプロジェクターの性能によっては細かい色の区別がつけれないことがある。

なので、できるだけ使う色の種類は減らしている。地の文である黒と、強調したい部分を示す赤色系くらいにしている。

その他好み

凝ったテーマを使うことに気恥かしさを感じるので、KeynoteのWhiteテーマ(白い背景に黒い文字)を使っている。 タイトルの位置や文字のサイズなどは変更している。

明朝体が好きなのでスライドに使ってる。堅苦しい雰囲気がでてしまうが、みんなそこまで気にしないだろと思って、好みを優先させている。

📝The reason for using reason #ML_study

ML勉強会 #2で話した。

要約

f:id:mzp:20170722144752j:plain

近年のJavaScriptは進化が激しく、様々な拡張が提案されている。その中にはMLの機能を類似したものも多数ある。例えば、オブジェクトとレコード型のように使えるようにする拡張パターンマッチを導入する拡張といったものが提案されている。

しかし既存の言語にMLを意識した機能を追加するのは簡単ではない。

そこでFacebookのReasonそこでOCamlJavaScriptに近づける、というアプローチを採用してる。具体的には文法をJavaScriptに近づけたり、周辺ツールの整備を行なっている。

JavaScriptの必要性

Misoca

f:id:mzp:20170722144756j:plain

ボクはMisocaという請求書を管理するWebサービスを作る仕事をしている。今日の交通費も出してもらった。ありがとう!

React/Redux

f:id:mzp:20170722144805j:plain

Webサービスを作るには、JavaScriptを扱う必要がある。

JavaScirptフレームワークは数多く存在しているが、そのうち有力なものの一つにReactとReduxがある。 Misocaでも採用している。

React

f:id:mzp:20170722144808j:plain

Reactはview、要はHTMLを構築するライブラリである。 今回の話とはあまり関係がないので、詳細は省略する。

Redux

f:id:mzp:20170722144813j:plain

ReduxはJavaScriptプリケーションの状態を管理するためのライブラリである。 以下のような型を持つ関数によって状態を遷移を管理する。

state -> action -> state

state はその名のとおりアプリケーションの状態であり、action はなんらかの操作を表す。 そしてこの関数はreducerと呼ばれます。 名前が一般的すぎますね。

Reduxの話はちょっとしたいので、reducerの例をいくつか見ていきます。

例: counter reducer

f:id:mzp:20170722144817j:plain

数を数えるreducerは上記のように定義できる。 アクションの種類(type)が INCREMENTAL なら1を足す、 DECREMENT なら1を引く、そうでない場合はそのままの値を返すようになっている。

これでカウンターの値という状態を管理する。

例: todo reducer

f:id:mzp:20170722144824j:plain

もうちょっとアプリケーションっぽいreducerも紹介する。 TODOの完了状態を切り替えるreducerは上記のように定義できる。

各項目は、固有のIDと項目の名前、完了したかどうかなどと項目として持っている。 アクションは種類をあらわすtypeフィールドに加えて、どの項目の完了状態を切り替えるかどうかを示すidフィールドを持つ。

そしてこのreducerはアクションが TOGGLE のときは、idを確認する。 そして、idが一致したら完了状態のだけを反転させた新しい状態を作って返す。

reducer idiom

f:id:mzp:20170722144828j:plain

このtodo reducerはreducerでよく使われるイディオムが2つ登場している。

1つ目は TOGGLE で使った「typeがxのとき、yという項目を持つ」である。これは、OCamlのヴァリアント型と似ている。

次は完了状態の切り替えで使っていた「状態をコピーし、一部だけ更新する」である。これはOCamlのレコードの更新と似ている。

ML由来の機能

f:id:mzp:20170722144831j:plain

このように最近のJavaScriptにはML系から様々な機能が輸入されている/されようとしている。

他に導入されているもしくは導入が提案されている機能としては

  • パターンマッチ
  • Maybeモナドのようなnullに対する演算
  • 非同期モナドのようなコールバックの連鎖を回避する文法
  • 静的な型検査

などがある。

more ML features

f:id:mzp:20170722144835j:plain

これらの機能を実現するために各種ツールを利用する。 例えば拡張されたJavaScriptからブラウザで動くJavaScriptを生成するためのBabelだとか、JavaScriptに静的な型検査を導入するflowなどを使う。

OCaml

f:id:mzp:20170722144839j:plain

あーあ、こんなときにバリアント/レコード型があって、パターンマッチがあって、静的型検査があってJavaScriptが生成できる言語があればなー。あー。

あっOCamlでいいじゃん。あってよかった。じゃあ使いましょう。 便利。 めでたし、めでたし。

JavaScriptOCamlのギャップ

f:id:mzp:20170722144843j:plain

これで終われる世界はだいぶ幸せだが、そうもいかない。

JavaScriptOCamlの間には大きなギャップが存在している。 それは文法だったり、ツールの使い方だったりする。

これReasonの紹介スライドにあった画像だが、途方にくれてる感があって最高だと思う。

OCamlJavaScript

f:id:mzp:20170722144847j:plain

しかしJavaScriptにMLの機能を導入するよりも、文法やツールを追加してOCamlJavaScriptに近づけるほうが楽そうである。 少なくともReasonの開発チームはそう考えている。

Reasonがやっていること

ではReasonがJavaScriptプログラマに使いやすくするためにやっていることについて話していく。

BuckleScriptとの連携

f:id:mzp:20170722144859j:plain

OCamlJavaScriptを出力できるように、BuckleScriptというOCamlからJavaScriptを生成するコンパイラと連携している。

上記の通り、BuckleScriptは人間にもかなり読みやすいコードを生成する。

人間に読みやすいコード

f:id:mzp:20170722144904j:plain

「人間にもかなり読みやすいコード」というのをBuckleScriptはかなり重視している。 これはREADMEにのってる例ですが、自動生成したとは思えないコードになっている。

中間言語であるlambdaからJavaScriptを生成しているこれができる、バイトコードから変換しているjs_of_ocamlとの重要な違いだ、とマニュアルに書いてあった。

余談: OCamlの魅力

f:id:mzp:20170722144908j:plain

BuckleScriptの資料が「なぜJavaScriptを使うか」という疑問に対しては、ブラウザで動く言語で…とかいろんな場所で動いて…とかいろいろと説明している一方、「なぜOCamlなのか」という疑問に対しては「もうしってるでしょ」ですませていた。格好いいと思う。

文法: 変数束縛

f:id:mzp:20170722144912j:plain

次は文法についてです。 JavaScriptの雰囲気にあうように文法が変更されている。

例えば変数束縛はletのあとのinをつけなっているし、行末には ; をつけるようになっている。

文法: 条件分岐

f:id:mzp:20170722144916j:plain

条件分岐も中括弧で範囲を示すようになっている。

文法: パターンマッチ

f:id:mzp:20170722144919j:plain

パターンマッチはmatchからswitchにキーワードが変更になっている。中括弧を明示してるのでネストしてもややこしくならない!ってマニュアルに書いてあった。

パッケージマネージャ

f:id:mzp:20170722144925j:plain

パッケージマネージャーはJavaScriptのnpmをそのまま使っている。 Reason自体も各種ライブラリのバインディングもnpm経由でインストールできる。普段と同じパッケージマネージャが使えるので、JavaScript使いには親切になっている。

が、npm install bs-platformするとOCamlのダウンロードとビルドがはじまるのはアツいと思う。

コード補完: Merlin

f:id:mzp:20170722144929j:plain

いくつかの周辺ツールも用意されている。 OCamlの補完ツールであるmerlinはそのまま使える。

エディタ拡張

f:id:mzp:20170722144932j:plain

各種エディタの拡張も用意されている。 Reasonはコードフォーマッタもあるので、それもエディタから使えるようになっている。

BeterErrors

f:id:mzp:20170722144937j:plain

既存のOCaml toolchainのラッパーもある。

例えばBetterErrorsコンパイラのエラーを整形し、 式のどの部分で型を間違えているかをわかりやすく表示する。 エラーメッセージの整形をしているだけなので、既存のコンパイラとパイプとつないで使うことができる。

RED

f:id:mzp:20170722144942j:plain

またocamldebugの使い勝手を改善するREDというツールもある。

余談: Reasonという名前

f:id:mzp:20170722144946j:plain

気付いてると思うが、Reasonという名前は検索しづらい。というかFacebookのだしてるライブラリはflowだとかinferだとか検索しにくい名前ばかりである。

Reasonという名前のせいで、exampleの名前がおもしろくなっている。 ライフゲームが実装されたイグザンプルのプロジェクト名がreason-of-lifeなのは最高だと思う。

Reasonの利用例

Reasonがどのあたりで使われているかの話をします。

Facebook Messanger

f:id:mzp:20170722144954j:plain

具体的にどの部分かは分かりませんがFacebookメッセンジャーの25%はReasonに書き換えられているらしい。このTweetには書いてないですが、ブラウザ版の話らしい。

React

f:id:mzp:20170722144957j:plain

Reactのプロトタイピングにも使われている。 JSXをサポートしてたりと、やたらReactのサポートが厚いのが気になっていたんですが、このためかもしれない。

Trello.md

f:id:mzp:20170722145001j:plain

ボクもReasonつかって、Chrome拡張を書いたChrome拡張のバンディングは存在してなかったので、必要な部分だけ自分で書いた。

Reasonのよい面

f:id:mzp:20170722145005j:plain

パターンマッチ最高

f:id:mzp:20170722145009j:plain

予想通りですがパターンマッチ+レコード更新の組み合わせは最高だった。 「各アクションのパラメータが型で保証される」「返り値の構造が変わっていないことが型で保証される」「すべての分岐を網羅していることをコンパイラが保証してくれる」というあたりがよい。

ビルド速度

f:id:mzp:20170722145013j:plain

BuckleScriptはOCamlのLambdaIRを変換した上で、末尾呼び出しの除去や定数畳み込み、インライン化などの最適化をなった上で、JavaScriptを生成する。 一見時間がかかりそうに見えるが、JavaScriptにしたあとの処理のほうが遅いので、あまり気にならなかった。

f:id:mzp:20170722145017j:plain

JavaScriptにはnullやundefinedみたいな「無」みたいな値がある。 これをfunctional扱えるようなモジュールが用意されている。

sがnullでない場合のみ内容を出力する関数、sがundefinedの場合のみ内容を出力する関数は上記のようになる。

callback hell

f:id:mzp:20170722145021j:plain

JavaScriptでは通信はコールバックで書く必要がある。それはいわゆるcallback hellを招く。このコードはPHPですが。

OCamlには非同期モナド(lwt)があるので、これを利用して >>= によるチェーンで書ける。 これは DelayedInc が発行されるのをまち、その後1000ms秒待ち、 Inc を送信しています。

Reasonのつらい面

次はつらい側面について話す。

インストール時間の増加

f:id:mzp:20170722145032j:plain

ReasonはOCamlコンパイラをforkして作っている。 そのため、インストール時にOCamlのビルドが必要になる。

そのため別マシンに移ったときなどにOCamlのビルドが毎回走ってしまう。CI上でOCamlのビルドが毎回はしるのでつらい。

大量の型定義

f:id:mzp:20170722145036j:plain

JavaScriptライブラリを使う箇所では、大量の型定義が必要になる。こんな感じの定義が延々と続くことになる。つらい。

JSON

f:id:mzp:20170722145040j:plain

JSONから情報を取得するのも大変で、一段ネストをすすめるためにパターンマッチが必要になる。

バリアントのランタイム表現

f:id:mzp:20170722145043j:plain

先ほど話した通りパターンマッチとバリアントの組み合わせは便利。

しかし、ランタイムではバリアントの名前が失なわれるため、デバッグがつらい。多相バリアントにするともっと難しくなる。

よくわかってない部分

Reasonに関してよくわかってない箇所の話をしていく。

vimハイライト

f:id:mzp:20170722145050j:plain

vimプラグインも準備されいるが、 キーワードのハイライトがおかしい。 たとえば when はキーワードだがハイライトされない。一方で普通の識別子である box はハイライトされる。

プラグインソースコードを読むとforked from rustと書いてあり、ハイライトがおかしい原因は分かる。 が、なんでそのままになっているか分からない。

JSCaml

f:id:mzp:20170722145055j:plain

JSCamlはFacebookが出しているJavaScriptOCamlに変換するコンパイラである。 念のためにもう一度いいますが、JavaScriptOCamlに変換するコンパイラである。

READMEに「ReasonでJavaScriptライブラリを使いたいときに、いったんJavaScriptにすると便利」とか「貧弱なデバイスJavaScriptを実行したいときに便利」と書いてある。なんなんだ。

esy

f:id:mzp:20170722145059j:plain

ReasonはJavaScriptのパッケージマネージャーnpmをそのまま利用している。 しかし、Reason/OCamlのようなビルドが必要な言語との相性はあまりよくない。

そこで、yarnをforkし、ソースコードの取得とビルドを明確に区別できるようにしたesyが開発されている。

opamブリッジ

f:id:mzp:20170722145103j:plain

またopamとのブリッジも用意されているので、npmのライブラリもopamのライブラリもインストールできる。そのため、npmのレポジトリに大量のopamのライブラリが登録されている。

迫力がある。

参考文献

f:id:mzp:20170722145107j:plain

この資料を作成するにあたって参考にしたものをあげておきます。

  • awesome-reason。reasonの各種資料へのリンク集。ツールや発表資料へのリンクがある。
  • Dawn of Reason。 途中で何度か図を引用してます。OCamlJavaScriptに近づければいいんだ!!という話が書いてあった。
  • Reason guide。reasonの文法などはここに書いてあったり書いてなかったりする。書いてないやつはOCamlからの連想でなんとかなる。
  • Bucklescript manual。 BuckleScript部分、つまりJavaScriptとの連携部分のマニュアル。が、文法はOCamlなのでReasonで使えたり使えなかったりする。

まとめ

f:id:mzp:20170722145112j:plain

  • JavaScriptにはMLの機能が必要で、どんどん導入されている。
  • でも大変なので、OCamlを使ったほうがいい。
  • そのままだと大変なので周辺環境を整備したのがReason

以上です。

✈️北海道ワーク

先週一週間は北海道の旭川で過した。 長期休暇を取ったわけではなく、昼間はリモートで勤務をした上で夕方から観光していた。

f:id:mzp:20170617083720j:plain

🏠宿

有給消化中の友人と一緒にAirbnbで部屋を借りた。 1週間でたいだい6万円くらい。

調理器具がついてたので、近所のスーパーで買ってきた魚やジンギスカンを焼いて食べていた。 焼くだけでだいたいうまい。

f:id:mzp:20170620084544p:plain

f:id:mzp:20170621001225p:plain

f:id:mzp:20170621001302p:plain

🏢リモートワーク

部屋についているWifiで仕事をしていた。通信速度などが不安だったが、問題なかった。念のため、市内のコワーキングスペースの場所を確認しておいたが、必要なかった。

普段通り仕事をしていたので、北海道にいることは気づかれなかった。 途中でTwitter経由でバレた。

👣市内観光

早めに仕事を開始して、17時くらいには仕事を終わらせ、その後、市内の観光をしていた。

櫻子さんの足下には死体が埋まっている」は旭川が舞台なので、市内の登場した場所にいくつか行った。びーとるのたびにっき 『櫻子さんの足下には死体が埋まっている』(TVアニメ版その①)~北海道・旭川エリア(旭川市・当麻町・美瑛町)~【舞台探訪(聖地巡礼)】がバスでの行き方も記載してあり、便利だった。

手。

街中に急に手がでてくるので、びっくりする。🙌。

f:id:mzp:20170612180455j:plain

ロータリー

f:id:mzp:20170611152624j:plain

ティーハウス ライフ・ラプサン

サイトによって日本最北の紅茶専門店と書いてあったり書いてなかったりする。 よく分からない。

f:id:mzp:20170613181556j:plain

シフォンケーキが登場していたので食べた。

f:id:mzp:20170613183558j:plain

ダンデリオン

ケーキ屋。ちょっと離れた場所にあるので、バスでは行けなかった。 レンタカーを借りた日に行った。

f:id:mzp:20170617173653j:plain

かぼちゃのモンブランが登場していたが、時期が違うらしく売っていなかった。モンブランは売り切れていた。

f:id:mzp:20170617211018j:plain

🍽食事

何を食べてもおいしかった。

うに

駅前のイオンで塩水うにが1500円くらいで売っていたので買った。 めっちゃうまい。

f:id:mzp:20170620233246p:plain

f:id:mzp:20170620233329p:plain

寿司

寿司は文句なしにうまい。

f:id:mzp:20170621090016p:plain

f:id:mzp:20170621085933p:plain

旭川ラーメン

旭川ラーメンも何店舗か食べにいった。

ぞい。

f:id:mzp:20170612195534j:plain

みそバターラーメンには、バターが予想の倍くらい入ってる。

f:id:mzp:20170613193028j:plain

地ビール

地ビールが何種類があるらしいので、飲みにいった。翌日寝坊すると飛行機に乗れなくなるという状況で飲むビールは、スリリングだった。

f:id:mzp:20170616180528j:plain

4種飲み比べセットを頼んだら、なぜか5種類でてきた。

f:id:mzp:20170617190635j:plain

🚗観光

週末+有給で周囲を観光した。

旭山動物園

一度行ったことがあるので、そこまで感動しないかなーと思ってたけど、すごかった。迫力がすごい。

f:id:mzp:20170614141805j:plain

f:id:mzp:20170620234552p:plain

f:id:mzp:20170620234647p:plain

f:id:mzp:20170614102301j:plain

青い池

macOSの壁紙になっている青い池を見にいった。 時期と時間が違うので、壁紙とはだいぶ印象が違うけどキレいだった。 次は冬に行きたい。

f:id:mzp:20170617094755j:plain

向う道はひたすらまっすぐだった。

f:id:mzp:20170620235207p:plain

富良野

青い池のあとは富良野に行った。 広大な土地に花が整然と植えてあるのは独特の迫力がある。

f:id:mzp:20170617114743j:plain

f:id:mzp:20170617154800j:plain

富良野オムカレー推してるので食べた。うまい。

f:id:mzp:20170617125930j:plain

✨感想

知らない街を散歩したり、地元の名物を食べるのが好きなので、働きながらしばらく滞在できるのはよかった。Airbnbブログで掲げられている「暮すように旅をしよう」はだいぶ好みにあっている。

ホテルに泊まるのに比べて自炊の割合が増えるのが心配だったか、スーパーで買ってきた何かを焼けばおいしくなるので問題はなかった。北海道だからな気はしている。

とはいえ、自宅に比べるとモニタの数が不足していたり、ネットワーク環境もそれほどよくなかったりと、めっちゃ作業が捗るという感じではなかった。

総合的によい体験だったのでまた行きたい。

📋Trello.md

TrelloボードをMarkdownに変換してクリップボードに書き込むChrome拡張を作った。

ボードの例

f:id:mzp:20170613223211p:plain

変換後のMarkdownの例(esaで変換した)

f:id:mzp:20170613223222p:plain

📦インストー

Trello.md - Chrome Web Store

Trelloのボードを開いた上で画面右上のアイコンをクリックすると、Markdown化されたボードの内容がクリップボードに書き込まれる。

f:id:mzp:20170613224109p:plain

💞動機

ふりかえりのKPTを書くのにTrelloを使っていると、内容のスナップショットを保存したいことがある。 その際、ボードの内容をMarkdownに変換してesaなどに保存すると便利である。

この用途のためにYusukeKokubo/trello2md: Export Trello Cards to Markdown formatというのがあるが、CLIツールで若干起動が面倒だったのでChrome拡張にした。

✨Reason

ソースコードhttps://github.com/mzp/trello.md/にある。

見ると分かるが、実装はReason + Bucklescriptで行なった。 ソースコードの割合をみても大半がOCamlになっている。

f:id:mzp:20170613225138p:plain

JavaScriptのような文法

Dawn of reasonにあるようにReasonはJavaScriptに文法を寄せようとしているらしい。

f:id:mzp:20170613225519p:plain

実際、いくつかの箇所でそのようになっていた。 例えば、OCaml(Bucklescript)ではレコードの構築、オブジェクトの構築、JSONの構築はすべて違う文法で行なうが、Reasonでは見た目を似せようとしていた。

// レコードの構築
let x = {
  x : 0, y : 1
};

// オブジェクト < x : int, y : int> の構築
let x = {.
  x: 0, y: 1
};

// JSON(Js.Json.t)の構築
let x = {
  "x": 0, "y":: 1
};

ただすべての行末に ; をつける制約はなくてもよかったんじゃないかな、とは思う。

Chromeバインディング

ChromeAPIに対するバインディングは必要な分だけ、自分で定義した。

わりと自然に書けた気がするがやはり大変。

module Tabs = {
  type tab = Js.t { .
    url : Js.Null.t string,                                                    
    id : int
  };

  module OnUpdated = {
    external add_listener: (int => unit => tab => unit) => unit = "addListener"
[@@bs.val] [@@bs.scope ("chrome", "tabs", "onUpdated")];
  }
};

vim-reason

https://github.com/reasonml/vim-reasonをいれたが、なぜか一部のキーワードがハイライトされなかった。

syntax fileを見てみたら “Forked from Rust” と書いてあり、 box などがキーワード扱いされてておもしろかった。

その他所感

  • しっかりと型がつくのはいいなぁ。
  • Reasonのビルドより、Webpackによるビルドのほうが時間かかってる気がする。
  • Reasonだけだと文法がわからず、Discordで聞いたりソースコードを読んだりしてた。

🔀bs-lwt

BuckleScriptで非同期処理を書きたかったので、Lwtバインディングを作った。 たぶんReasonからも使えると思う。

github.com

🙅 制限

Lwtの一部はUnixモジュールなどに依存しているのでBuckleScriptからは使えない。 なのでコア部分(src/core以下にあるモジュール)のみを使えるようにした。

利用できるモジュールの一覧はREADMEに書いてある。

🎯動機

BuckleScriptでredux-sagaみたいなのが作りたかった。 そこで、redux-sagaのソースコード読んでみたところ、タスクスケジューラを実装していることが分かった。

タスクスケジューラを再実装するのは大変なので、軽量スレッドのライブラリであるLwtのバインディングを作った。

「redux-sagaみたいなの」は今のところ ripple_task.mli みたいになっている。 Lwt_list.map_s のような関数で合成できるので、ジェネレータで実装したタスクより使いやすい気がしている。

😵大変だった箇所

submodule

最初、Lwtのソースコードはgit submoduleを使ってレポジトリに追加していた。 npmはsubmoduleに対応してるのでしばらくはうまくいっていたが、途中でyarnが対応していないことに気がついた。

しょうがないので、ソースコードをレポジトリに直接追加した。

バージョン番号

Lwtのバージョンとバンディングのバージョンの対応を考えるのが大変そうだったので、この2つを一致させるルールにした。

そしたら今度はバンディングのビルド方法を修正したときにバージョンをあげる方法がなくなってしまった。 試しに v3.0.0.1 みたいに4桁目を使ってみたが、semverではなくなりインストールできなくなってしまった。

postinstall

最初は、bucklescript-addons/bs-mochaを参考に、postinstallででライブラリをビルドするようにしていた。

が、ReasonのDiscordで聞いてみたら、

  • postinstallでビルドするのは古いスタイルで、今はなにもする必要はない
  • ライブラリを利用するプロジェクトが bsb -make-worldすれば、依存ライブラリもビルドされる
  • 出力するモジュールの種類を後で変更できるので、このほうが柔軟である

とのことだったので、消した。

💞所感

githubにpushだけしておいたら、Discordで晒されてて勢いを感じた。 typoを修正するプルリクエストも来た。

f:id:mzp:20170528210650p:plain

🌴沖縄

地域を変えれば花粉症が軽減するのがどうかに興味があったので沖縄にいってきた。 沖縄は梅雨の時期だということは出発前日まで気づいてなかった。

f:id:mzp:20170521134054j:plain

🌊座間見

とまりんに行ったら、ちょうど座間見行きのフェリーがでてたので乗った。1年前に来たときは海が荒れてて乗れなかったけど、今回は乗れた。

何も計画せずに行ったら、レンタカー屋が閉まってた。 「もしかして事前に予約しないとオープンしないタイプの店では????」という話をしてたが、真偽は分からない。

f:id:mzp:20170526202456p:plain

しょうがないので、徒歩でいける砂浜に移動した。

港から砂浜の間は山道になっている。

f:id:mzp:20170521134054j:plain

海。

f:id:mzp:20170521130320j:plain

どうしようもないので寝転がって時間を潰す。

f:id:mzp:20170521131754j:plain

暇すぎて動画を作ってた。

f:id:mzp:20170526211155g:plain

🔦鍾乳洞

別の日は玉泉洞を見にいった。

鍾乳洞みたいなーという軽い気持ちではいったら、かなりの距離を歩いて大変だった。 今しらべたら890メートルあるらしい。わりとじめじめしてるし、圧迫感のある中を歩くのは大変だった。

f:id:mzp:20170526203802p:plain

キレいだけど、青いライトアップをして青い泉と名乗るのはどうかと思う。

f:id:mzp:20170526203407p:plain

あと終盤電波が届かないことにつらみを感じつづけていた。

☕️コーヒー

うわさの泡盛コーヒーは買った。 あとで飲もうと思ってホテルの冷蔵庫で冷してたら、そのまま忘れてきたので飲めていない。

f:id:mzp:20170526205141p:plain

ノンアルコールのコーヒーはスタバで飲んだ。

f:id:mzp:20170526205320p:plain

🌡気温

ちょうど名古屋に夏日が来てる時期だったので、沖縄のほうが涼しかった。

🍴食事

写真を見たら肉ばっかりだった。

座間見で食べたたラフテー。これを食べてる間にレンタカー屋が閉まったという説もある。

f:id:mzp:20170526204340p:plain

ステーキ。

f:id:mzp:20170522114255j:plain

唯一あった肉じゃない写真。オジサン(魚の名前らしい)を塩で煮たやつ。

f:id:mzp:20170521191511j:plain

行きたかったけど満員で入れなかったところ。2000円で飲み放題、食べ放題はちょっとおかしいと思う。

f:id:mzp:20170526204746p:plain

f:id:mzp:20170526204742p:plain

✨まとめ

🔬redux-saga

redux-sagaの動きを調べた。

redux-sagaは redux-sagaで非同期処理と戦うで説明されているように、非同期処理などを直列プログラムのような形式(直接形式; direct style) で書くためのライブラリである。 そのためにタスクを導入し、その切り替えを制御している。

複数のタスクを協調制御するという点で、コルーチンや軽量スレッド、fiberなどに類似していると感じた。

🔎対象

redux-saga v0.15.3を対象とする。ただし一部コードは説明のためにエラー処理や終了処理を省略する。

また counter-vanilla を元にした以下のプログラムの動きを追う。

// counter.js
//////////////////////////////////////////////////////////////////////////
//  Reducerの定義
// INCREMENTが来たら +1 する reducer
function counter(state, action) {
  if (typeof state === 'undefined') {
    return 0
  }
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    default:
      return state
  }
}

//////////////////////////////////////////////////////////////////////////
//  Sagaの定義
const effects = ReduxSaga.effects
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))

// INCREMENT_ASYNC の1秒後にINCREMENTを発生させる。
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC')
    yield effects.call(delay, 1000)
    yield effects.put({type: 'INCREMENT'})
  }
}

//////////////////////////////////////////////////////////////////////////
//  redux-sagaの初期化
const createSagaMiddleware = ReduxSaga.default
const sagaMiddleware = createSagaMiddleware()
var store = Redux.createStore(
    counter,
    Redux.applyMiddleware(sagaMiddleware))

//////////////////////////////////////////////////////////////////////////
//  タスクの初期化・実行
sagaMiddleware.run(counterSaga)

//////////////////////////////////////////////////////////////////////////
//  イベントハンドラ
document
  .getElementById('incrementAsync')
  .addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT_ASYNC' })
  })

これは Increment async ボタンを押すと、1秒後にカウンタがインクリメントされるプログラムである。

f:id:mzp:20170507225036g:plain

🚀redux-sagaの初期化

reducer等を定義したのち、以下のようにredux-sagaの初期化を行なう。

// counter.js
const sagaMiddleware = createSagaMiddleware()
Redux.createStore(
    ...,
    Redux.applyMiddleware(sagaMiddleware))

createSagaMiddleware

createSagaMiddleware は以下のように定義されている。

// src/internal/middleware.js
export default function sagaMiddlewareFactory({ context = {}, ...options } = {}) {
  // 渡されたオプションが妥当であることを確認する
  if(logger && !is.func(logger)) {
    throw new Error('`options.logger` passed to the Saga middleware is not a function!')
  }
  // sagaMiddlewareを定義する
  function sagaMiddleware({ getState, dispatch }) {  /* snip */ }

  // 定義した関数を返す
  return sagaMiddleware
}

引数が妥当であることを確認をした上で、内部で定義した sagaMiddlewareを返す。

sagaMiddleware.runの初期化

sagaMiddlewareapplyMiddleware の内部で呼び出される。 この関数は以下のような定義されている。

// src/internal/middleware.js
function sagaMiddleware({ getState, dispatch }) {
  // sagaMiddleware.run を初期化する
  sagaMiddleware.run = runSaga.bind(null, {
    context,
    dispatch,
    /* snip */
  })

  return next => action => { /* snip */ }
}

sagaMiddleware.runrunSaga を代入し、sagaの実行をできるようにする。 この際、 Function.prototype.bind を使って dispatch などのReduxとやりとりするために必要な関数が runSaga に渡されるようにしている。

Reduxのミドルウェアとして動く next => action => .... については、イベントハンドラの動きを追う際に見る。

🏃タスクの作成・実行

// counter.js
sagaMiddleware.run(counterSaga)

sagaMiddleware.run によって counterSaga の実行が開始される。

タスクの生成

sagaMiddleware.run には runSaga が代入されている。 これは以下のような定義となっている。

// src/internal/runSaga.js
export function runSaga(
  storeInterface,
  saga,
  ...args
) {
  // ジェネレータを呼び出す
  let iterator = saga(...args)

  // タスクを作成する
  const task = proc(
    iterator,
    /* snip */
  )

  return task
}

saga(...args)counterSaga を呼び出している。 これはジェネレータなので、ここではイテレータが返るだけで関数本体は実行されない。

ここで作ったイテレータprocに渡し、タスクを生成する。 proc は以下のようなコードになっている。

// src/internal/proc.js
export default function proc(iterator, /* snip */) {
  // タスクを作る
  const task = newTask(parentEffectId, name, iterator, cont)

  // タスクを実行するnext を呼ぶ
  next()

  // タスクを返す
  return task

  // タスクを実行する関数
  function next(arg, isErr) { /* snip*/ }
}

newTask によって、タスクを管理するオブジェクトを生成している。 その後、タスクを実行する nextを呼び出したのち、タスクを返している。

タスクの実行

next は以下のようなコードで定義される。

// src/internal/proc.js
function next(arg, isErr) {
  // イテレータを進め、次のyieldまでを実行する
  let result = iterator.next(arg)

  // 返ってきた値に応じて処理をする
  runEffect(result.value, parentEffectId, '', next)
}

iterator.next(arg)イテレータを進め、その返り値をrunEffect に渡している。

runEffect での処理が完了したのち、タスクの実行を再開できるようにするため、 runEffect には自分自身である next を渡している。

📤アクションを待つ

counterSaga は以下のように定義されているので、イテレータが進めたれた際に yield effects.take('INCREMENT_ASYNC') まで実行される。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC') // <- ここまで実行される
    yield effects.call(delay, 1000)
    yield effects.put({type: 'INCREMENT'})
  }
}

effects.take('INCREMENT_ASYNC') の返り値は以下のようなオブジェクトになっており、これがそのままiterator.next() の返り値になる。

{
  "@@redux-saga/IO": true,
  "TAKE": { "pattern": "INCREMENT_ASYNC" }
}

このオブジェクトが runEffect に渡されると以下のような分岐を経て、 runTakeEffect に渡される。

// src/internal/io.js
// どの種別のエフェクトなのかを判定するための関数群を定義する
const TAKE = 'TAKE'
const createAsEffectType = type => effect => effect && effect[IO] && effect[type]
export const asEffect = {
  take : createAsEffectType(TAKE)
}

// src/internal/proc.js
function runEffect(effect, parentEffectId, label = '', cb) {
  let data
  // effectの種類に応じて、専用の関数を呼ぶ
  return (
    // Non declarative effect
      is.promise(effect)                      ? resolvePromise(effect, cb)
    : is.helper(effect)                       ? runForkEffect(wrapHelper(effect), effectId, cb)
    : is.iterator(effect)                     ? resolveIterator(effect, effectId, name, cb)
     // declarative effects
    : is.array(effect)                        ? runParallelEffect(effect, effectId, cb)
    : (data = asEffect.take(effect))          ? runTakeEffect(data, cb)
    : (data = asEffect.put(effect))           ? runPutEffect(data, cb)
    : (data = asEffect.all(effect))           ? runAllEffect(data, effectId, cb)
    : (data = asEffect.race(effect))          ? runRaceEffect(data, effectId, cb)
    : (data = asEffect.call(effect))          ? runCallEffect(data, effectId, cb)
    : (data = asEffect.cps(effect))           ? runCPSEffect(data, cb)
    : (data = asEffect.fork(effect))          ? runForkEffect(data, effectId, cb)
    : (data = asEffect.join(effect))          ? runJoinEffect(data, cb)
    : (data = asEffect.cancel(effect))        ? runCancelEffect(data, cb)
    : (data = asEffect.select(effect))        ? runSelectEffect(data, cb)
    : (data = asEffect.actionChannel(effect)) ? runChannelEffect(data, cb)
    : (data = asEffect.flush(effect))         ? runFlushEffect(data, cb)
    : (data = asEffect.cancelled(effect))     ? runCancelledEffect(data, cb)
    : (data = asEffect.getContext(effect))    ? runGetContextEffect(data, cb)
    : (data = asEffect.setContext(effect))    ? runSetContextEffect(data, cb)
    : /* anything else returned as is        */              cb(effect)
  )
}

チャンネルへの登録

runTakeEffect は以下のような定義となっている。

// src/internal/proc.js
function runTakeEffect({channel, pattern, maybe}, cb) {
  channel = channel || stdChannel
  channel.take(cb, matcher(pattern))
}

// src/internal/channel.js
function take(cb, matcher) {
  cb[MATCH] = matcher
  takers.push(cb)
}

runTakeEffect では、タスク間の通信に使われるチャンネルに対して take を呼び、 チャンネルの takers 配列に cb を追加している。 この cbnext であるため、あとでこれを呼び出せばcounterSaga の実行が再開できる。

ここまでで sagaMiddleware.run の実行は完了し、redux-sagaの初期化が完了する。

👀イベントハンドラ

イベントハンドラを見ていく。

// counter.js
document
  .getElementById('incrementAsync')
  .addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT_ASYNC' })
  })

Increment async ボタンがクリックされると、Reduxのディスパッチャに INCREMENT_ASYNC アクションが渡される。

アクションの配信

先程は省略した sagaMiddleware は以下のように定義されている。

// src/internal/middleware.js
function sagaMiddleware({ getState, dispatch }) {
  const sagaEmitter = emitter()
  // ....
  return next => action => {
    // 次のミドルウェアにアクションを転送する
    const result = next(action)

    // アクションを配信する
    sagaEmitter.emit(action)
    return result
  }
}

次のミドルウェアにそのままアクションを転送することで、reducerを起動する。 その後、アクションを sagaEmitter.emit に渡す。

emitter

emitter は以下のように定義されており、 emit されると対応する subscribers が起動する。

// src/internal/channel.js
export function emitter() {
  const subscribers = []

  function subscribe(sub) {
    subscribers.push(sub)
    return () => remove(subscribers, sub)
  }

  function emit(item) {
    const arr = subscribers.slice()
    for (var i = 0, len =  arr.length; i < len; i++) {
      arr[i](item)
    }
  }

  return { subscribe,  emit }
}

チャンネル

チャンネルの take で利用していた stdChannel は以下のように定義されている。

// src/internal/proc.js
export default function proc(
  iterator,
  subscribe = () => noop,
  /* snip */
) {
  // procの引数として渡されたsubscribeを用いてチャンネルを作成する
  const stdChannel = _stdChannel(subscribe)
  ....
}

// src/internal/channel.js
export function _stdChannel(subscribe) {
  // eventChannel を用いてチャンネルを作成する
  const chan = eventChannel(cb => /* snip */)

  return {
    ...chan,
    take(cb, matcher) { /* snip */  }
  }
}

export function eventChannel(subscribe, buffer = buffers.none(), matcher) {
  const chan = channel(buffer)

  // 何かがemitされた場合は、それをチャンネルにputする
  subscribe(input => {
    chan.put(input)
  })

  return {
    take: chan.take,
    flush: chan.flush
  }
}

eventChannel で入力をそのままチャンネルに put する関数を登録している。 そのため、ディスパッチャに渡されたアクションが、チャンネルへと put される。

チャンネルへのput

チャンネルの put は以下のように定義されている。

// src/internal/channel.js
function put(input) {
  // takers配列が空の場合はバッファに追加する
  if (!takers.length) {
    return buffer.put(input)
  }

  // takers配列に関数が登録されている場合は、それに入力を渡す
  for (var i = 0; i < takers.length; i++) {
    const cb = takers[i]
    if(!cb[MATCH] || cb[MATCH](input)) {
      takers.splice(i, 1)
      return cb(input)
    }
  }
}

takers 配列に格納されている関数に入力を渡している。 今回は next が登録されているため、counterSaga の実行が再開される。

つまり、redux-sagaのミドルウェアからの put (emit)と、counterSagatake がチャンネルを挟んで対になって動作する。

f:id:mzp:20170508000833p:plain

🤝 プロミスの実行

counterSaga は 以下のように定義されているので、実行が再開されると effects.call(delay, 1000) まで実行される。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC') // <- さっきはここまで実行した
    yield effects.call(delay, 1000) // <- ここまで実行される
    yield effects.put({type: 'INCREMENT'})
  }
}

take の場合と同様に、この返り値は runEffect 内の分岐を経て、 runCallEffect に渡される。

// src/internal/proc.js (再掲)
function next(arg, isErr) {
  // イテレータを進め、次のyieldまでを実行する
  result = iterator.next(arg)

  // 返ってきた値に応じて処理をする
  runEffect(result.value, parentEffectId, '', next)
}

// src/internal/proc.js (再掲)
function runEffect(effect, parentEffectId, label = '', cb) {
  let data
  // effectの種類に応じて、専用の関数を呼ぶ
  return (
    // Non declarative effect
      is.promise(effect)                      ? resolvePromise(effect, cb)
    : is.helper(effect)                       ? runForkEffect(wrapHelper(effect), effectId, cb)
    : is.iterator(effect)                     ? resolveIterator(effect, effectId, name, cb)
     // declarative effects
    : is.array(effect)                        ? runParallelEffect(effect, effectId, cb)
    : (data = asEffect.take(effect))          ? runTakeEffect(data, cb)
    : (data = asEffect.put(effect))           ? runPutEffect(data, cb)
    : (data = asEffect.all(effect))           ? runAllEffect(data, effectId, cb)
    : (data = asEffect.race(effect))          ? runRaceEffect(data, effectId, cb)
    : (data = asEffect.call(effect))          ? runCallEffect(data, effectId, cb)
    : (data = asEffect.cps(effect))           ? runCPSEffect(data, cb)
    : (data = asEffect.fork(effect))          ? runForkEffect(data, effectId, cb)
    : (data = asEffect.join(effect))          ? runJoinEffect(data, cb)
    : (data = asEffect.cancel(effect))        ? runCancelEffect(data, cb)
    : (data = asEffect.select(effect))        ? runSelectEffect(data, cb)
    : (data = asEffect.actionChannel(effect)) ? runChannelEffect(data, cb)
    : (data = asEffect.flush(effect))         ? runFlushEffect(data, cb)
    : (data = asEffect.cancelled(effect))     ? runCancelledEffect(data, cb)
    : (data = asEffect.getContext(effect))    ? runGetContextEffect(data, cb)
    : (data = asEffect.setContext(effect))    ? runSetContextEffect(data, cb)
    : /* anything else returned as is        */              cb(effect)
  )
}

Promise.prototype.thenへの登録

runCallEffectは以下のように定義されている。

// src/internal/proc.js
function runCallEffect({context, fn, args}, effectId, cb) {
  // callの引数に渡された関数を起動する。  
  let result = fn.apply(context, args)

  // 返り値としてプロミスが返ってくるので、resolvePromiseに渡す
  return resolvePromise(result, cb)
}

function resolvePromise(promise, cb) {
 // Promise.prototype.then にコールバック関数を登録する
  promise.then(
    cb,
    error => cb(error, true)
  )
}

call エフェクトに渡された delayrunCallEffect 内で呼び出される。 その返り値となるプロミスは、resolvePromise に渡される。 resolvePromise 内では、Promise.prototype.thencb を登録する。

この cbcounterSaganext であるので、プロミスの実行が完了したのち counterSaga の実行が再開される。

🔈アクションのディスパッチ

プロミスの実行が完了したのち、nextによって effects.put({type: 'INCREMENT'}) まで実行が進む。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC')
    yield effects.call(delay, 1000) // <- さっきはここまで実行した
    yield effects.put({type: 'INCREMENT'}) // <- ここまで実行される
  }
}

take エフェクトや put エフェクトの場合と同様に、 runEffect を通じて runPutEffect が呼び出される。

Reduxへのディスパッチ

runPutEffect は以下のようになっている。

// src/internal/proc.js
function runPutEffect({action, resolve}, cb) {
  // Reduxのdispatchに引数を渡す
  let result = dispatch(action);

  // コールバック関数にその結果を渡す
  return cb(result)
}

引数に渡された {type: 'INCREMENT'} をそのままReduxのディスパッチャに渡す。 これにより counter reducer が動き、カウンタの値がインクリメントされる。

その後、cb に代入された next を呼び、counterSaga の実行を継続する。 counterSagaは以下のように定義され、effects.take('INCREMENT_ASYNC') までループし、これまでと同様の処理が続いていく。

// counter.js
function* counterSaga() {
  while(1) {
    yield effects.take('INCREMENT_ASYNC') // <- ここまで戻る
    yield effects.call(delay, 1000)
    yield effects.put({type: 'INCREMENT'}) // <- さっきはここまで実行した
  }
}

✅まとめ

簡単なシーケンス図にまとめると以下のようになる。 直列的に実行されるように書かれている counterSaga の処理が何度も中断され、条件が満たされるたびに実行が再開されている。

f:id:mzp:20170507223441p:plain

このようにredux-sagaではタスクの切り替えを制御することで、特定のアクションが来るのを待ったり、プロミスの完了を待つなどの処理を、直接形式で書けるようにしている。