ジンジャー研究室

長めのつぶやき。難しいことは書きません。

スケーラブルなプログラミングのために何が必要か

f:id:jinjor:20150516071422p:plain

Fluxに関する独自解釈と妄想を、何かの翻訳っぽく書いた。

スケールするアーキテクチャ

フレームワークを作る時、我々は「簡単に記述する」ことを第一に考えがちだ。 そして、簡単にするための仕組みはウケる。 逆に記述量が増えるとウケない。

しかし例外があって、多く書くことによるメリットが受け入れられたときは別だ。 例えば、Backbone.jsを使うと記述量が増える事は誰もが認めるところだが、MVCの実現というメリットのために広く受け入れられた。 要するにトレードオフなのだ。

ここのところFluxアーキテクチャが注目を浴びているが、書いてみると記述量は相当増える。 そもそも登場人物が多すぎる。 Action、Dispatcher、Store、View、それからそれらの間に挟まって仕事をする者達。 一体彼らは何をしたいのか。

最近になって分かってきた。 これはアプリケーションそのものを抽象化したアーキテクチャなのだと。

Fluxは何がしたいのか

FluxはMVCを置き換えるものとして登場した。 本当にそうだろうか。

Actionは必要か

例えば、AngularJSやBackbone.jsはActionを必要とせず、そのままモデルを更新する。 あるいはビューが直接モデルを更新するのは責務上よろしくないとか、ロジックの共通化という目的で、例えばコントローラに関数を置いたりする。 しかしActionは登場しない。 むしろ直接関数を呼び出す方が手軽なのでは?

結論から言うとActionは必要だ。

全ての出来事はデータだ

データベースを普通に使うと大抵は最後に反映された状態のみを保存しているため、それがどういう経緯で作られた状態なのか復元できない。 しかし、全てのイベントを保存しておけば、過去から現在の全ての状態が再現できるという考え方がある。 そうは言っても無限のリソースを持っているわけではないので、どこかで情報を圧縮することにはなるのだが、確実に言えることはイベントの集積が情報として一番強いということだ。 そして関数を適用するにしたがって、計算結果を得る代わりに次第に情報量は減っていく。

こうしておけばDBに専用のフラッシュバック機能は必要ない。

出来事はそれがどのような用途に使われるかを知らない

「いつ誰がどこで何をした」という出来事は、特定の用途には情報過多かもしれない。 しかし安易にそれを捨ててはいけない。 後からアドホックに追加された機能によって、別の可能性を見出されるからだ。

ある機能は「何をしたか」によって処理を切り替えるかもしれない。 しかし後から追加された機能が「どこでそれが行われたか」でフィルタリングして分析を開始するかもしれない。

順番に届いた出来事を好きな人が好きなだけ持っていく

Actionを実際にDBに保存するかはさておき、何らかの手段でそれらはキューの形で運ばれてくる。

同じ情報を使う機能がいくつもあるということになれば、Pub/Subモデルが適している。 そういうわけでDispatcherが登場する。 DispatcherはただPub/Subの仕組みを提供するだけで、具体的にどんなSubscriberがいるかには無関心だ。

スケーラビリティは不可逆

このように順序立てて考えていくと、Fluxが見えてくる。

ここで重要なのは、このアーキテクチャ最も理想的なアプリケーションの構造を抽象化したものだということだ。 実際に必要か否かに関わらずActionは存在するものだ。だからそれを表現した。

そうなると書きやすさは二の次だ。 おそらく目の前の書きやすさにフォーカスするのなら、必要なのはFluxではなくAngularJSだ。 Fluxはもっと大規模を想定する。

最近よく考えるのは「スケーラビリティは不可逆」だということだ。

サーバー台数を横に増やす話ではなく、ここで言っているスケーラビリティとはなるべく同じコードを保ったままアプリケーションの構成を変えられるという意味だ。

マイクロサービスが話題になっているが、何も考えずにモノリシックに書き始めるとマイクロサービスに拡張することは永久に不可能だ。 そこで、じゃあ最初から考えろよという話に普通はなるのだが、ここで追求する理想は、考えなくても拡張できる状態になっていることだ。 言い換えれば、小さいインフラでも大きなサービスと同じ書き方をしておいてスケーラビリティを確保せよという話だ。 小さいうちから大げさに?いや、大げさであると思わせないほど簡潔に表現するのだ。

そのためには、何かしらの言語なりフレームワークが必要だ。 Fluxが出てきた時に確かに「MVCはスケールしない」と言っていたのだが、MVCのスパゲッティ状態を解消する目的という話に発散してしまったようだ。 それで、なるほどAltMVCかと思って考え始めたのだが、どう考えてもActionが必要なかった。

しかし、スケーラビリティに注目すると色々と辻褄が合う。 そうなると全然フロントエンドだけの話ではない。 全てを支配できる。

以降、追加でスケーラビリティ実現に何が必要かを考えてみる。

本質的でない状態を排除する

主にサーバサイドにおいてスケーラブルなアーキテクチャを指向してアプリケーションを書き始めると、ひとつの気付きがある。

状態を管理する必要が全くないという事だ。

昔からステートレスにしなさいとは言われていたが、Amazonに至っては思い切ってLambdaと表現するなどしている。

言ってしまえば、アプリケーションとはアクションと古い状態を入力として新しい状態を返す関数ということになる。 型をつけるとこうなる。ちなみにモナドではない。

application :: Action -> State -> State

ここでいう状態というのは例えばDBなどの事を指していて、決して計算途中の値のことではない。 forループの外にあるsum変数などは本質的な状態ではない。あるいは設計の都合上たらいまわしにして構築されるオブジェクト、これも状態を持つ必要はない。 言い換えれば全て純粋な関数で書けるということだ。

関数型言語が必要

しかし我々は「慣れていて書きやすい」という理由で不用意に状態を扱ってしまう。

例えば先ほどの関数で、新しい状態を返す代わりに古い状態を書き換えたらどうなるだろうか。 関数の呼び出し側は新旧の値の比較が出来なくなってしまう。 実際、このことがReact.jsの最適化を妨げる要因となっていて、Immutableを売りにした類似フレームワークは軒並みパフォーマンスが高い。

他にも有名な例としては、リストから新しいリストを作るときにmap関数を使うかforループを使うか、というものがある。

val newList = oldList.map(_ + 1)

簡潔に書けているが、問題はそこではない。 重要なのは、既に並列計算のための準備が出来ているということだ。

val newList = oldList.par.map(_ + 1)

for文でこうはいかない。 手続き型言語では、知らず知らずのうちにスケーラビリティを落としているケースがあるのだ。

何故か。 状態を変更する方法だけを提供すべき関数がそれを実際に適用してしまったり、各リスト要素の変換方法だけを提供すべき関数がリストの作り方にまで言及しているからだ。 こういう事が平気で起きてしまうのは、純粋関数型言語以外は副作用の有無を区別しないからだ。

しかもだんだん規模が大きくなってくると、どこでそういうことが行われているかが全く分からなくなる。 そしていざという時になってHadoopへの移行は無理だね、という話になる。

デフォルト非同期

また少し違う観点で話をすると、Node.jsのような非同期ベースは最早当たり前にあって良い。

Node.jsで現状不満なのは、非同期のほうがコード量が増えるということと、非同期APIと同期APIが全く別の書き方を要求するということだ。 しかしこれはシンタックスの問題なので、非同期処理が簡単に書ける言語があれば何の問題も無い。

非同期処理を同期処理の切り替えが自由になるのは都合が良い。 例えば、JavaScriptではlocalStorageが同期APIなのだが、抽象化のために非同期APIでラップするとIndexed DBとの乗換えが楽になる。

もっと言うと、RPCを使ったコードを綺麗に書ける可能性を秘めている。 先ほどの.parのように簡単に切り替えられるとか。

最強の抽象化で勝負に出る

総合すると、スケーラブルな言語やフレームワークの要求仕様とは次のようなものだ。

  • Actionをデータとして扱う ⇒ 通信手段、再現性に対して柔軟
  • Dispatcherを使ったPub/Sub ⇒ 機能拡張に対して柔軟
  • Immutableなデータと純粋な関数を使う ⇒ 並行性、物理構成に対して柔軟
  • デフォルト非同期 ⇒ 同期処理と非同期処理の切り替え、通信手段に対して柔軟

最初から「疎結合」と言えばそれで済んだのかもしれないが、それではコードレベルに落ちないのでこれで良い。

あとはこれを超書きやすくするだけだ。

書きやすくなければ意味が無い。 特にImmutabilityや非同期処理の書きやすさは言語レベルのサポートがないと無理だ。 頑張れば出来るかもしれないが、やりたくない。 そろそろJavaScriptを捨てる時が来ているのかもしれない。

余談だが、Immutabilityはフロントエンドからサーバ、クラウド、インフラ、DB、どこへ持っていっても良いものだという感触がある。 色んな意味でリソースが贅沢に使えるようになったおかげだろう。

React.js+Fluxをやるなら今すぐElmを使うべき理由

皆さん、そろそろElmやりましょう。

Elmって何なの?

Webブラウザで動くFRP(Functional Reactive Programming)言語です。 コンパイルするとHTMLやJavaScriptを吐き出します。

Elm

公式サイトに動くサンプルが大量にあるので見てみると面白いです。

どうして今やるの?

これまでElmと言えば、良くも悪くも理想を追求した言語で、一般的なWebの部品(HTML/CSS/JavaScript)と相性が悪く、「まぁちょっとCanvas使っておもちゃアプリでも作るかー」くらいが関の山だったのですが、最近になってその状況は一変しました。

  • HTMLライブラリのサポート
  • Ajaxなど非同期タスクのサポート
  • JavaScriptAPIを通じて相互接続可能
  • エコシステムの登場

順序はちょっと忘れましたが、0.14とか0.15で色々出来るようになりました。

import Html exposing (..)
main = h1 [] [text "Hello, world!"]

Webブラウザで動くのにHTMLライブラリが無かったと言うのは意味が分からないかも知れませんが、実はElmは現在のWebの仕組みにとらわれずゼロから考えて作られています。冷静に考えると、そもそもマークアップ用の言語でアプリケーションを作るとか、そっちの方が意味が分かりませんよね?

本当は理想を突き進んでWebのしがらみを全部なくしてしまいたいのですが、現実問題としてはまだ普通にHTMLで組む方が楽できるよね、ということで使いましょうHTML。

あとはライブラリがそろっていない部分をNative(JavaScript)で補えるのも大きいです。 これで既存資産も活用できます。ちょろっと試したところWebSocketも出来ました。

React.jsやFluxと何の関係があるの?

一言で言うと、書き方が似ています!!

一応リアクティブ繋がりではあるんですが、最近リアクティブという言葉がバズりすぎていて最早この言葉を使う意味がありません。どちらもHTMLを宣言的に記述できるようになっています。

見るのが早いので見に行ってください。言語作者が自前で書かれているTodoMVCのソースです。

https://github.com/evancz/elm-todomvc/blob/master/Todo.elm

わずか約350行でTodoアプリ全体が表現されています。

Elm作者のEvanという方は以前からMVC的な設計を強く意識していて、コードに色濃く現れています。冒頭のコメントを以下に抜粋します。

 1. Model  - a full definition of the application's state
 2. Update - a way to step the application state forward
 3. View   - a way to visualize our application state with HTML
 4. Inputs - the signals necessary to manage events

React.js+Flux流に解釈するとおよそ次のようになります。

名前 役割
Model アプリケーションの状態を定義する。FluxのStoreに相当。
Update 状態を更新する。FluxのDispatcherに相当。
View 状態をHTMLとして表現する。ReactのComponentに相当し、FluxのActionを発行。
Inputs 入力を管理する。

と書いていますが、何か特別な言語仕様やフレームワークがあるわけではありません。単にそういう風に分けて設計しましたという話です。 実際にはAjaxなどでサーバとやり取りする必要が出るはずですが、大枠は変わらないと思います。

そして、DOMのレンダリングを高速化するためにVirtual DOMが使われています

実際、View部分の記述は本当にReact.jsのrender()そのままで、コンポーネントツリーの上から順にデータを渡して宣言的にHTMLを記述していきます。イベントをアクションとして発行して最終的にコンポーネントツリーにフィードバックさせる構造はFluxと同じです。

言語としてのアドバンテージ

React.jsをやっていると、JavaScriptの様々な言語仕様の壁にぶちあたります。 その点でもElmには以下のような追加の恩恵があります。

Immutability

JavaScriptでは新旧の値を比較する時、古い値を誤って更新しないために並々ならぬ努力が必要です。Immutabilityを実現するためにはライブラリで頑張るしかありません。

Elmの言語仕様は細かい点を除いてHaskellとほとんど同じで、変数ももちろんImmutableです。

型付け

Elmはコンパイル時に静的に型チェックを行います。 JavaScriptとの接続部分に関しては、ブート時にAPIが定義されていることをチェックするのと、Elm側にオブジェクトを渡す際に型のチェックが動的に入ります。

まとめ

React.js+Flux的な事がしたい人にとってElmは既に有力な選択肢です。 実際にプロダクションで使うには信頼と実績と見えない力が必要ですが、その辺は気合で何とかしましょう。

ついでと言っちゃなんですが、FRPの流儀が一緒に学べるのもおいしいところです。FRP単体のJavaScriptライブラリを導入するのはメリットが薄くて却下になることが多いと思うので。

というわけで、アーリーアダプターになってElmの実績を作っていきましょう。

以上。

Herokuと同程度に簡単なOpenShift使用メモ(on Windows)

Heroku有料化に伴いOpenShiftを試した。その時のメモ。

以下、簡単と言いながら色々躓いているがドキュメントは凄く親切なので、基本的にはこれで足りる。

developers.openshift.com

アカウントを作る

Webページから(省略)。

コマンドラインツールのインストール

コマンドラインツールrhcがgemで提供されているので、まずはRuby1系の最新をインストール。 2系だとエラー。

gem install rhc

依存関係で怒られた。

ERROR:  While executing gem ... (Gem::DependencyError)
    Unable to resolve dependencies: rhc requires highline (~> 1.6.11); commander requires highline (~> 1.7.1)

~> 1.6.11という指定は1.6.Xまでしか受け付けないようだ。

gem install highline -v 1.6.11

再度。

gem install rhc

また怒られた。

ERROR:  While executing gem ... (Gem::DependencyError)
    Unable to resolve dependencies: commander requires highline (~> 1.7.1)

gem install rhc failed. Gem::DependencyError · Issue #678 · openshift/rhc · GitHub gemをアップデートしろと。

gem install rubygems-update
update_rubygems
gem install rhc

また怒られた。今度はgemのエラーっぽい。

ERROR:  While executing gem ... (ArgumentError)
    invalid byte sequence in UTF-8

Windows固有の問題らしいので以下を参考に修正。 会社の PC で 2 時間ほど悶々と悩んでたこと。 - msfukuiの日記

win32/registry.rb

#FormatMessageA = Kernel32.extern "int FormatMessageA(int, void *, int, int, void *, int, void *)", :stdcall
FormatMessageW = Kernel32.extern "int FormatMessageW(int, void *, int, int, void *, int, void *)", :stdcall
def initialize(code)
  @code = code
  #msg = "\0".force_encoding(Encoding::ASCII_8BIT) * 1024
  msg = "\0\0".force_encoding(Encoding::UTF_16LE) * 1024
  #len = FormatMessageA.call(0x1200, 0, code, 0, msg, 1024, 0)
  len = FormatMessageW.call(0x1200, 0, code, 0, msg, msg.size, 0)
  #msg = msg[0, len].force_encoding(Encoding.find(Encoding.locale_charmap))
  msg = msg[0, len].encode(Encoding.find(Encoding.locale_charmap))
  #super msg.tr("\r", '').chomp
  super msg.tr("\r".encode(msg.encoding), '').chomp
end

再度。

gem install rhc

出来た。

コマンドラインツールのセットアップ

rhc setup

色々訊かれるので、問題なければデフォルト値とyesをひたすら選択。 途中、さっき作ったアカウントでログイン。 SSH鍵の作成から公開鍵を登録するところまでやってくれる(秘密鍵は手動で読み取り専用にしろと書いてある)。

Node.jsアプリケーションの作成

多分Node.js以外も同じようなプロセスになるとは思う。 URLはhttp://アプリ名-ドメイン名.rhcloud.com/になるので慎重につける。(後からでも変更できるけど面倒)

アプリケーション作成Webからでも出来るが、ここではコマンドで作成。 Node.jsの場合はカートリッジ名をnodejs-0.10とする。

rhc create-app home nodejs-0.10

最初のアプリケーションを作る場合はここで名前空間ドメイン)を訊かれるので入力。

怒られた。

The authenticity of host 'home-jinjor.rhcloud.com (54.166.221.17)' can't be established.
RSA key fingerprint is 
Are you sure you want to continue connecting (yes/no)?

ここに至る前に仮に作ったドメインが気に入らなくて消したりしたせいかも。 もう一度セットアップ。

rhc setup

Web画面からアプリを消して再度作成する。

rhc create-app home nodejs-0.10

今度は通った。IPアドレスやら何やらが付与される。

この時点で、http://home-jinjor.rhcloud.com/に既にデフォルトアプリが立ち上がっているので見に行く。

開発を進める

先ほど作ったアプリのルートに移動して、Gitのリモートリポジトリを一応確認しておく。

git remote -v

何故か.gitignoreが無かったので追加。

echo node_modules > .gitignore

後はいつものようにNode.jsアプリを起動。

npm install
node server

http://localhost:8080/にアクセスして確認。

ソースも確認。

self.ipaddress = process.env.OPENSHIFT_NODEJS_IP;
self.port      = process.env.OPENSHIFT_NODEJS_PORT || 8080;

Herokuと同じく、主要な情報を環境変数から取得しているのが分かる。 Herokuと同じなら、DB等のアドオンの情報も環境変数から取得できるはず。

修正したらプッシュ。

git push origin master

自動的に再デプロイされる。

感想

Herokuと同じ。

HTTP/2で再帰的にPUSH_PROMISEするための最速アルゴリズム

話の発端

サーバプッシュするリソースの関連付けを全部手動で書くのが面倒だから、動的に中身を読んで解決したい。 その時に、依存の深いところにあるものでも、リクエストのストリームを閉じずに待たないといけないという制約があった。

jinjor-labo.hatenablog.com

というわけで、依存が深くてもなるべく早くコンテンツを返すためのアルゴリズムを考えた。形式的な説明が思いつかないので具体例を挙げる。

例1

index.htmlがstyle.cssを必要とし、style.cssがback.pngを必要としている。(階層の深さ:2)

index.html

<link rel="stylesheet" href="style.css"></link>

style.css

body { background: url("back.png"); }

最適なレスポンス順序

index.htmlに直接関連するリソース(ここではstyle.css)をPUSH_PROMISEした時点でindex.htmlのコンテンツを返す(この時点ではストリームを閉じない)。それ以上深いリソースのPUSH_PROMISEがすべて終わってからストリームを閉じる。

f:id:jinjor:20150407115139p:plain

back.pngを待たずにindex.htmlの全体を送って良い。なぜなら、ブラウザがindex.htmlをパースしてもback.pngが必要だという情報は得られないから。

例2

index.htmlがstyle.cssとapp.jsを必要としている。(階層の深さ:1)

index.html

<link rel="stylesheet" href="style.css"></link>
...
<script src="app.js"></script>

最適な順序

ストリームで細切れにすればより速い。ブラウザがサブリソースをリクエストしないことを保証したら、その部分のコンテンツを直ちに送りつける。

f:id:jinjor:20150407121006p:plain

まとめ

  • なるべく早くコンテンツを返してブラウザのパースを早める
  • 実装大変そう

権限の委譲と表現の自由度

難しい話ではないが、思考の整理のために書いてみる。


f:id:jinjor:20150327013947j:plain

図は、左の人が右の人に「この材料で何か喋って」と頼む様である。 下に行くほど右の人に大きな権限と表現の自由が与えられている。


f:id:jinjor:20150327014313j:plain

同じ挨拶でも「Hi」と言わせたい時は、権限の委譲が不十分だと左の人が面倒を見ることになる。


f:id:jinjor:20150327014726j:plain

逆にもっと丸投げしてみる。ユーザーIDだけ渡すから何とかしろ。 DBにアクセスしに行って、このユーザは現在英語で表示する設定にしているから名前は「Taro Yamada」が適切だろう。


f:id:jinjor:20150327015155j:plain

でもそれだと通信が沢山発生するのでキャッシュから取ってもらおう。


f:id:jinjor:20150327015254j:plain

いやもうむしろ全部お前に任せた。 必要なものはSessionスコープなりrequestスコープから全部取れ。


f:id:jinjor:20150327015658j:plain

そうなると必然的に任された方はアプリケーションについて十分な知識を持っている。 そうではなく、汎用的なユーティリティとして振舞う場合は細かくデータを渡す形式が向いている。


f:id:jinjor:20150327015854j:plain

React.jsみたいに、コンポーネントを組み立てるフレームワークが増えてきた。 図では、検索を行うために<TodoSearch>コンポーネントに上からオプションを渡している。


f:id:jinjor:20150327020116j:plain

<TodoSearch>コンポーネントの位置が変わって、上位のノードに変更が入ってしまった。


f:id:jinjor:20150327020224j:plain

これを防ぐひとつの解決策としては、contextのような何でもボックスを全てのコンポーネントに渡すようにする。


f:id:jinjor:20150327020308j:plain

もうひとつは、グローバルに何でもボックスを転がしておく。


f:id:jinjor:20150327020343j:plain

やはり、汎用的なコンポーネントとは区別する。


以上。

proxyquireでNode.jsのrequireをモックする

発端

React+Fluxのアプリのテストをするのに、requireのモックが必要だった。 だけどJest使いたくない。

そこでこれを使う。

github.com

簡単な使い方

something.js

module.exports = 'music';

love.js

var something = require('./something.js');

console.log('I love ' + something + '!');

index.js

var proxyquire = require('proxyquire');

require('./love.js');// => I love music!

proxyquire('./love.js', {
  './something.js': 'shit'
});// => I love shit!

require('./something.js');をモックして何でも言わせちゃう。

ユニットテスト用途には十分

requireで指定する文字列の曖昧さを許容しないのがちょっと微妙だけど、ユニットテストに使う分にはまぁ問題にはならない。

呼ばれた回数をカウントしたり色々したい場合は、Sinon.jsを使うのが便利。

DOM要素のidはグローバル変数

DOM: element IDs are global variables

知らなかった…orz

とはいえ普段は使わないほうがよさそう。

既に定義されているグローバル変数と被っていると上書きされない。

  <div id="a">Hello!</div>
  <script>console.log(a);//<div id="a">Hello!</div></script>


  <script>var b;</script>
  <div id="b">Hello!</div>
  <script>console.log(b);//undefined</script>


  <script>c = null;</script>
  <div id="c">Hello!</div>
  <script>console.log(c);//null</script>


  <script>d = undefined;</script>
  <div id="d">Hello!</div>
  <script>console.log(d);//undefined</script>


  <script>e = undefined;</script>
  <script>delete e;</script>
  <div id="e">Hello!</div>
  <script>console.log(e);//<div id="e">Hello!</div></script>