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

ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

Rails+Reactアプリをbrowserifyからwebpack基盤に移行した件

ちょうど一年ほど前、「チェンクロ パーティーシミュレーター」(以下ccpts)を、
jQueryを使った制御から、React.js + Bacon.jsで大幅に書き換えました...φ(・ω・`)

parrot.hatenadiary.jp

ccpts.parrot-studio.com

github.com

そもそも、ccpts自体、
「モバイルで動くように*1、できるだけ今風の技術でクライアント側を構築する」
という目的を持って設計してました

昨年の時点ではReactが十分にメインストリームに乗ったと判断して、
基本的なアーキテクチャをReact+browserifyに載せ替えたわけですが、
「Reactで動くようにする」ことが目的だったので、細かいところは置いておいたわけです

それを今回、フレームワークを見直してきれいにした(している)というお話です(´・ω・)っ

解決したい問題とwebpack

昨年末、チェンクロで第3部が実装され、
システムが大幅にアップデートされました

こうなると、ccptsのシステムも3部対応で大幅に書き換えがいるわけですが、
以前構築してからここまで運用してみて、
最大の問題は「デプロイに時間がかかりすぎる」ということです

サーバサイドのデータを書き換える分には問題ないのですが、
クライアント側のjsをいじるとデプロイ時にbuildが必要になり、
私が使っているVPSだと10分以上かかってしまいますΣ(゚Д゚)ガーン

3部対応するにあたり、クライアント側を頻繁にいじる必要があるので、
さすがにこれは許容できません

そもそも、これだけコストがかかると、
「クライアントをいじらずにどうにかできないか?」と考えるようになってしまい、
本来的な設計から遠ざかってしまいます(´-ω-)

そこで、2016年末の時点での「今風のやり方」を探すことにしましたのですが、
その時点で主流になっていた(ように見えた)のがwebpackです

そもそも、Railsは「Asset Pipeline」がよくできすぎています
これを自前で実装するとコストがかかるので、
何とかして今風のコードと組み合わせて乗っけたい・・・というのがポイントになります

調べた結果、ざっくりとこんな感じのやり方が主流になっているように見えました...φ(・ω・`)

  1. クライアント側のコードを閉じた形で独立して構築する(Railsの管理外)
  2. webpackで一つのjsにまとめて、Railsのassets管理下に置く(これはpureなjs)
  3. あとはRailsのprecompile等に素直に任せる

当初、これを自前で書こうと思ったのですが、
もっと楽ができないのか・・・と思ったら、
「React on Rails」という良いものがあったわけです

github.com

1年前の記事でも最後に取り上げていましたが、
この時点では何が主流になるのかさっぱりわからなかったので、
そこまでは踏み込みませんでした(´-ω-)

このやり方の最大の決定打は、Rails5.1におけるDHHの方針です

weblog.rubyonrails.org

github.com

Rails用に仕組みを構築しちゃうあたりがさすがですが、
このwebpackerにreactのオプションも入っているくらいで、
もう迷う要素はなくなりました( ゚д゚)o彡゚

まだRails5.1はリリースまで時間がかかりますが、リリースされたとしても、
React on Railsにはreactのサーバサイドレンダリングという仕組みがありますので、
最終的にはRails5.1の上でReact on Railsという形になると思います

とはいえ、サーバサイドレンダリングをいきなり使うと、
React on Railsに依存しすぎてしまうので、まずは使わずに、
「今風の開発フローを構築するための基盤」として利用しております

実際の段取り

CoffeeScriptの排除とnpmからyarnへの移行

「できるだけクライアントのコードを client 以下に移す」というのが方針になりますが、
es6でクラス定数が使えない等々で、CoffeeScriptで書いたmodelが残っている箇所がありました

これらはAsset Pipelineとbrowserifyで動作していたわけですが、
webpackで一つにまとめるためには、全てes6で書き換える必要があります(´-ω-)

そこで、es6で書くことを優先して、クラス定数の方をあきらめました

(例としてmodelを一つ)
https://github.com/parrot-studio/cc-pt-viewer/blob/69c8e88185536e9c9c8af67c2b5e5caa53069f66/client/app/bundles/ccpts/model/Favorites.js

今回は目的が違うので、この手の妥協は仕方ありません(´・ω・`)

ついでに、npmによるパッケージ管理をやめて、yarnに切り替えました

yarnpkg.com

Railsでbundlerを使っているわけで、この仕組みは全く違和感がありません

import/exportを正しく使う

以前は各modelやcomponentを呼び出す前に、
ブラウザのグローバルにBaconやReactのような各ライブラリを突っ込んでおき、
暗黙的に参照するというやり方でとりあえずやってました

これはCoffeeScriptとes6を共存させるのに必要だったのですが、
CoffeeScriptを排除したので、もう不要です
全部import/exportで書き換えました...φ(・ω・`)

問題は、どうしてもimport漏れが出てくることです

当初、動かしてみてはエラーを見て追加・・・みたいな、
超絶に効率の悪いことをしていたのですが、
ESLintで全部解決しました(`・ω・´)

eslint.org

ルールは自分の直感に反しないレベルでこんな感じに(´・ω・)っ

cc-pt-viewer/.eslintrc at master · parrot-studio/cc-pt-viewer · GitHub

これでエラーが出ないようにガンガン修正していきました
rucobopと同じく、自動で修正する機能もあるので、とても楽です

jQueryの取り扱い

これは1年前と何も変わってませんが、Reactを使うからといって、
jQueryを "使うべきではない" という意見には賛同できません( ゚Д゚)y─~~

たしかに、jQueryで "DOMの操作をすべきではない" ですが、
jQuery界隈には大量のライブラリやノウハウの蓄積があり、
それを全てReactの文脈に落とし込めるとは思えないわけです*2

実際の現場で、jQueryなら数分でできたことが、
Reactだと3日かかります・・・といって、
企画側に納得してもらえませんよね(´・ω・`)

ギリギリまでjQueryを使わないようにしつつ、
どうしてもjQueryが優位な箇所だけ使っていく・・・
それは1年前の時点と変わっていません

しかし、以前はライブラリを全部グローバルに定義していたので問題ありませんでしたが、
全部閉じた環境に持っていこうとすると、jQueryの取り扱いがとたんに難しくなります(lll゚Д゚)

ここに関しては情報も少なく、相当試行錯誤したのですが、
結果的に「古いライブラリは古いやり方、新しいライブラリは新しいやり方」と、
完全に切り分けることにしました

具体的にはこんな感じです...φ(・ω・`)

https://github.com/parrot-studio/cc-pt-viewer/blob/0f289cc18827a5135388c77199fd7477fb174d0f/app/assets/javascripts/application.js

どうしてもjQueryの文脈が必要なものだけassets以下で管理して、
Reactを含む他の一切をwebpackで一つにまとめたわけです

React側からは「$」がグローバルアクセスできるので、
あとはReactのrefで生DOMにアクセスしてjQueryのアニメーションを適用するだけです
1年前の記事参照)

グローバル変数があるとESLintが警告を出すのですが、
.eslintrcで「$というグローバル変数だけ許可して」と定義しているので問題ありません
むしろこれにより、「$というグローバル変数が存在する」ということが明確になります(`・ω・´)

置き換えた結果と今後

方針を決めたのが11月で、12月に入ってからは実装されたキャラデータの暫定登録を進めて、
そこから基板入れ替え作業を進めましたが、12月末にはざっくり作業が完了しました

それ以降の開発速度は目に見えて速くなりまして、
やはり心理的な障壁がぐっと下がったのはでかいです(`・ω・´) b *3

コストが下がったことで、棚上げしていた問題、
例えば、Becon.jsのStreamをもっと整理したいとか、
そもそもUIを大きくいじりたいとか、そういうところにも手を出せるようになりました

でも、一番改善したいのはReact on Railsの機能である、
「Reactのサーバサイドレンダリング」なのですが、
そのあたりは今後作業予定なので、できあがったらまた書きます...φ(・ω・`)

*1: 基準としては「iPad miniで見た時に快適であること」です いつも使っているので(`・ω・´)

*2: もちろん、長期的には対応していくはずですが、現時点ではReactを「普通の人」がいじるにはコストが高すぎ、対応ライブラリの整備がすぐに進むとは思えません(´-ω-)

*3: svnからgitに移ることで、branchを切るコストが飛躍的に下がり、現在のような開発フローが構築された・・・というのと同じですね

Elixirについて調べたついでに、BrainF**kインタプリタを書いてみた

最近、某D社の事例を含めて、Elixirの話題をよく見かけるようになりまして

http://elixir-lang.org/

もちろん、話題になっているからには、
何らかの「時代の要請」があるはずでして、そのあたりを調べてみたわけです

なぜ、Elixirが出てきたのか?

まず読んでみた本はこちらです(´・ω・)っ

プログラミングElixir

プログラミングElixir

この本は言語の使い方について書いた本ではなく、
「設計思想」について書かれた本でして、
そのこと自体がElixirの出自を象徴しております

歴史の流れを復習してみると、かつてWebはPerlJavaがメインで、
普通のWebであればだいたいPerlCGIで書かれていたわけです

でも、ある程度大きな機能になるとPerlではきつくなってきて、
「もっとちゃんとした言語」が求められ、
RailsをきっかけにしてRubyがブレイクしました

Rubyのポイントは、まつもとさん自身が書いているように・・・

・・・多くの言語が「機能」からの要請で生まれてきたのに対し、
Rubyは「コードをこう書きたい」とか「こう書けると気持ちいい」といった、
「設計思想」を優先して生まれてきた、という経緯があります

その分、(特に1.9より前においては)実行速度に難があったのですが、
システムの大規模化・複雑化や、CPU速度の飛躍的向上により、
メリットがデメリットを大幅に上回るようになったわけです(`・ω・´)

一方で、時代はPCからモバイルへ変わっていき、
「大きなリクエストを素早く」ではなく、
「小さくて大量のリクエストを素早く」という要求に変わっていきました

古いアーキテクチャではこの要請に応えられない*1ため、
Nginxが登場したり、Apacheアーキテクチャが変わり、
さらなる高速化のためWebSoketやそれに対応するNode.jsが出てきたりしたわけです

そして、CPUのアーキテクチャも変化しました
単純な速度ではもはや向上が見込めないため、
マルチコアにするのが当たり前になってきています

その結果、「小さくて軽い処理(をするAPIサーバ)」が求められるようになり、
Rubyではこれらの状況に対応できない・・・ということはないのですが、
Railsもスリム化できるようになり、Node.jsと組み合わせたりするようになりました

(勘違いしないでもらいたいのは、Rubyがダメということではありません
 Rubyの強みは「時代の要請についていくセンス」と、
 「変化を許容し、時に古いものを切り捨てる強さ」にあるので)

一方で、「小さく軽い処理のを組み合わせて一つの大きな処理を構成する」方式は、
まさに関数型言語が得意とする領域です

しかし、関数型言語は難しすぎます
古くはLispHaskellがありますが、これを「普通の人」が扱うのはとても無理*2です(´-ω-)

その領域をScalaが埋めていくのかな・・・と思っていたのですが、
(少なくとも私は)某D社以外に大きな事例も聞かず、
「Play Framework」のWebにおける大きな採用事例も(少なくとも私は)知りません

そもそも、当の某D社がScalaを切った・・・とまではいかずとも、
Scalaで書き直しました!」と大々的にアピールしていたシステムを、
すでにElixirで書き直しているあたりで、だいだい状況は推測できます(´・ω・`)

他の関数型言語だと「F#」あたりなのですが・・・
あれは本を読んでみたものの、難易度的にはScalaと大差はなく、
(少なくとも現状では)Windowsっぽいイメージが強すぎてきついです

難易度を無視すれば、「小さなロジックを大量のプロセスで処理する」*3という、
理想的な設計で事例もある「Erlang」も使えるはずなのですが、
パラダイムのジャンプだけでなく、文法的な難しさがつきまといます

こんな状況の中、「関数型言語をもっとなじみがある文法で」、
より端的にいえば「Rubyが書きやすいんだから、Rubyみたいに書けて、
Erlangのように動く言語
があればいいんじゃない?」と考えた人がいたわけです

それがまさに「Elixir」でして、冒頭でも書いたように、
Elixir最大のポイントは「こう書けるとうれしい」という、
「設計思想」を優先して生まれた言語である、ということなのです(`・ω・´) b

ElixirでBrainF**kインタプリタを書いてみる

先ほどの本を読んだ感じ、設計思想にはものすごく共感できたので、
とりあえず私にとって「関数型言語におけるFizzBuzz」である、
「BrainF**k」の実行系を書いてみました...φ(・ω・`)

以前もScalaで書いたことがありますし・・・

parrot.hatenadiary.jp

・・・それより前にはRubyで「実行系を作成する実行系」を書いたりもΣ(・ω・ノ)ノ

で、今回Elixirで実際に書いてみたのがこちらです(´・ω・)っ

gist.github.com

大きなポイントは二点ありまして、まず一つ目は
「データの処理の流れを記述する」ということを強く意識させる文法です

  def execute(code) do # codeを実行するとは・・・
    code        # 受け取ったcodeを・・・
      |> parse  # 解釈し・・・
      |> eval   # 実行することである
  end

  defp parse(str) do # 解釈するとは・・・
    str  # 受け取った文字列を・・・
      |> String.replace("\n", "")  # 改行を取っ払って・・・
      |> String.codepoints  # 一文字ずつに分離し・・・
      |> List.foldl([], fn c, li -> li ++ [@cmap[c]] end)
        # コマンドのストリームに置き換えることである
  end

この書き方はとてもいいですね(`・ω・´) b

「|>」は前の関数の結果を次の関数の第一引数として渡す、
というだけの構文でしかないのですが、
これだけで「何かしたいのか?」がとても明確になります

それこそが「関数型言語風に宣言的なコードを書く」ことの、
最大の利点であり、それ自体は他の言語でも享受できるのですが、
文法として明確に組み込まれているのがポイントです

もう一点が、「関数の引数自体がパターンマッチになっている」ことです

  defp step(:pinc, coms, jump_map, ind, cind, pind, buf, result) do
    step(coms[cind+1], coms, jump_map, ind+1, cind+1, pind+1, buf, result)
  end

  defp step(:pdec, coms, jump_map, ind, cind, pind, buf, result) do
    step(coms[cind+1], coms, jump_map, ind+1, cind+1, pind-1, buf, result)
  end

# 略

  defp step(_, _coms, _jump_map, _ind, _cind, _pind, _buf, result), do: result

Javaでいうオーバーロードに近いのですが、
ScalaRubyでいうところのcaseやswitchのような、
パターンマッチングを、関数の定義自体でやっています

先ほど挙げた、以前書いたScalaコードを見てもらえばわかるのですが、
stepに相当するところの中でパターンマッチングしていて、
また自身を再帰的に呼び出しています

そのため、stepの処理自体が肥大化しているし、
新しいコマンドを追加する場合、case文のマッチングを増やさないといけません(´-ω-)

その点、Elixirのコードは一つ一つの関数が「定義」として小さくまとまっており、
「何をしたいのか?」が明確になって、見通しが良くなっています

「これだけのこと」ではあるのですが、
それでも「こう書けるととても楽」というのを優先したのがElixirでして、
まさにそれはRubyと同様の設計思想なわけです(`・ω・´)

Elixirは「使える」のか?

こんな感じで、私の好みにあうElixirですが、
今の私の仕事である「アーキテクト」の立場からすると、
少なくとも今は「ない」かな・・・とΣ(゚Д゚)ガーン

弊社CTOとも話をして、「この言語はいいね(`・ω・´)」という話にはなっていますが、
あくまでお仕事として考えると、あまりにもコストが高いかなと思います

「表面的な文法」はRubyに似ているため、
例えばRubyの初期のように「PerlのようにRubyを書く」のと同様、
RubyのようにElixirを書く」のは可能ですし、私もそのように導入します

しかし、Elixirの利点を最大限の享受するには、
「関数型的な設計思想」が必須になります

ElixirがRubyに強く影響を受けているのと同様に、
ElixirのWebフレームワークである「Phoenix Framework」は、
とてもRailsの影響が強いです

www.phoenixframework.org

「ある程度わかっている人ならば」Railsの仕組みをこれに移すことも可能かもしれません

しかし、RubyRailsも最大のポイントは、先ほど書いたように、
「時代の要請についていくセンス」と
「変化を許容し、時に古いものを切り捨てる強さ」です

RubyはどんどんVMが高速化されていますし、
文法的にも「型」を導入しようとしています
(普通の言語なら危険信号なのですが、まつもとさんならまず大丈夫でしょう)

Railsもモジュール化・スリム化(API専用モードなど)が進み、
並行処理を意識したライブラリを組み込んだりもしています

github.com

なにより、個人的な感覚では、「ElixirでRDBにアクセスすること」自体が、
せっかくのメリットを台無しにしていると思いますΣ(・ω・ノ)ノ

Node.jsの仕事をしたときに強く感じたのは、
とにかく「全てを非同期に書かなければならない」ということであり、
「同期的な処理をどう切り離すか?」を常に考えないといけません

こういったことを意識しなければならないのは、
少なくとも「普通のWebシステム」においてはオーバースペックですし、
例えばRubyでもそういった設計思想のシステムは十分組めます*4

あとは人的コストの問題です
Node.jsを書いたことがある人はそこそこ探せますし、
Rubyを書ける人なら(質を問わなければ)十分見つかるでしょう
(なぜ、FBような大規模システムで、PHPが使われているのかを考えてみてください)

今後、Elixirが何らかの形でメインストリームに乗った場合*5は別として、
現時点では「あえてRubyを切り捨てる」メリットがあまりに薄いのかな・・・と思います

メインのWebサーバをRuby+Railsで組み、
リアルタイム性を求められる領域をNode.jsで構築する、というのが、
今の時点では「普通にいけそうなWebシステム(=長く運用できるシステム)」だと思います

逆に「普通じゃないWebシステム」、
例えば「大量の動画をバックエンドでリアルタイムにエンコードし続けるシステム」ならば、
かなり有効に作用すると思います

・・・が、そういうシステムを必要とする会社は限られてますし、
そういう会社はシステムにかけられるコストが桁違いなので、
やはり「普通の会社」では難しいですよね・・・(´・ω・`)

*1: いわゆるC10K問題

*2: ここでいう「無理」とは、「扱える人がいない」のではなく、「扱える人を雇うコストが、一般のWeb企業において現実的ではない」くらいの意味で

*3: このプロセスをクラウド上で起動するのが「サーバレスアーキテクチャ」とか話題のあれですね

*4: というか、以前仕事で組みました http://parrot.hatenadiary.jp/entry/2013/09/03/112657

*5: 個人的にはそうあってくれると楽しいのですが・・・(´-ω-)

続きを読む

RailsをRedisで「効率よく」高速化してみる(+おまけ)

仕事でコードを書く時間が減ると、別なところでコードを書きたくなるもので、
久々にチェンクロパーティーシミュレーター(以下ccpts)の
システム部分をいじっていました...φ(・ω・`)

ccpts.parrot-studio.com

以前react化したり、Rails5に置き換えたりしたわけですが・・・

parrot.hatenadiary.jp

parrot.hatenadiary.jp

・・・どうしても「遅い」ってのが問題でして(´-ω-)

react化=表示自体を全てJavaScriptで制御となると、
JSを解釈するまでメイン部分が描画されないわけで、
どうしても体感で遅くなってしまいます

「部分的react化」により、見かけ上の描画を早くすることは可能ですが、
私のスキルが追いついていないので、それはいったん置いておいて・・・Σ(・ω・ノ)ノ

私にできるのは「APIの速度改善」かな・・・ということで、
APIの速度を高速化してみたわけです

Redisのキャッシュを「効率よく」使う

APIが遅い原因はわりとシンプルで、たいていの場合は「(同期的な)I/O」です
ある程度並列処理が許される計算処理に対し、
同一のリソースに触るI/Oは排他制御等の「待ち」が発生します

I/Oといっても「ファイル」とか「ネットワーク」とかいろいろありますが、
「ネットワークでアクセスするファイルI/O」が遅いのは間違いなく、
端的にいえば「DBはどうしても遅い」ということに(´-ω-)

以前だと、DBのデータをメモリに全部乗せるとかいろいろありましたが、
KVS(現在はほぼRedis)の登場により
「高速で非同期アクセスが可能なネットワークストレージ」が登場しました

DBに対して処理時間のオーダーが文字通り「桁違い」なので、
これをキャッシュ的に生かせば、DBへのアクセスが減り、速くなる・・・はずです

だからといって、何でもかんでもRedisに突っ込めばいいわけではありません
Redisもネットワークストレージなわけで、
自分のメモリ上で処理が完結するならそっちの方が速いわけです

postd.cc

この記事は非常にすばらしく、参考になります(`・ω・´) b

そもそも、「Key-Value Store」という名の通り、
「Keyに紐付いたデータ」を取り出すのは非常に高速ですが、
「Keyのリスト」とか取り出そうとした時点で破綻しますΣ(゚Д゚)ガーン

実際、私も以前MongoDBで作ったシステムをMariaDBで作り直しています

parrot.hatenadiary.jp

用途が増え、串刺しな検索する機能が増えた結果、
RDBのような構造化された構造の方が圧倒的に速くなったわけです

つまり、当たり前のことですが・・・

  • 参照が圧倒的に多い
  • 結果取得に計算時間がかかる
  • 頻繁に内容が更新されない
    • ライフサイクルを明確に定義できる
  • Key-Valueという単純な構造に落とし込める
    • 「Keyのリスト」にアクセスする必要がない
  • 非同期アクセスしても問題ない

・・・(Redisに限らず)こういう条件を満たせなければ、
キャッシュを生かせないか、無駄に複雑化してバグの温床になるだけです(´・ω・`)

ccptsでRedisキャッシュを使う

これをふまえてccptsのAPIを高速化していくわけですが、
ccptsのAPIは概ね「検索に対してアルカナデータのリストを返す」という構造をしています

以前はクエリに対してDBを検索し、アルカナデータを構築して返していたのですが、
一つのアルカナに関連するテーブルがたくさんあり、
効率的に取得してもどうしても遅くなるわけです(´-ω-)

しかし、「アルカナのデータ」は「データの更新」をしない限り一意で、
だからこそブラウザ側でもキャッシュさせたりしているので、
「アルカナのID - アルカナデータ」という構造をキャッシュしようと

つまり、「DBで検索してDBからデータを取得」から、
「DBでIDだけ取り出して、IDに対応するアルカナデータをRedisから取得」に変えるわけです

ここで重要なのは、検索自体はDBにやらせることです
「構造化されたデータの検索」は圧倒的にDBが高速ですからね・・・

他にも、「検索画面用に返しているマスターデータ」とか、
初期表示に使う「最近追加されたアルカナリスト」とか、
決め打ちで返せるものもキャッシュしてしまいます

問題は「キャッシュの生存期間」とか「更新」についてですが、
今回の場合「データの取り込み時」にキャッシュも更新してあげれば解決です

古いキャッシュにアクセスされる可能性も、
「Keyにデータバージョンを含む」ことで回避できます(`・ω・´)

以上を実装して、先日リリースしたのがこちらです

redisのキャッシュで高速化 · parrot-studio/cc-pt-viewer@42947de · GitHub


<Redisキャッシュ対応前>

f:id:parrot_studio:20161031094518p:plain

<Redisキャッシュ対応後>

f:id:parrot_studio:20161031094603p:plain

<Redisキャッシュ対応後(ブラウザで時間測定)>

f:id:parrot_studio:20161031094611p:plain

文字通り、桁が違いましたΣ(゚Д゚;≡;゚д゚)


まあ、元々は仕事でもりもり設計していた部分ではあるのですが、
仕事のコードは持ち出せなくても、ノウハウを自分のアプリに反映しておけば、
別なところに使い回しできますからね・・・

ちなみに、お仕事の場合はメモリキャッシュ(Railsのmemstore)も使ってまして、
扱うデータに応じてメモリかRedisか選んで格納するようにしています*1

あと、キャッシュはあくまでキャッシュであって、
「消えてもDBから"必ず"復元できる」というのも徹底しています
Redisを信用してないわけではないですが、データの安全性はRDBが圧倒的ですしね

assetsの取り扱いを変える

これで体感でもそこそこ速くなったので、
Google先生のパフォーマンスチェックサイトで計測してみたのですが・・・

testmysite.thinkwithgoogle.com

・・・モバイル/PCでそれぞれ40点とさんざんでしたΣ(゚Д゚)ガーン

アドバイスに「データを圧縮して返せ( ゚Д゚)y─~~」と書かれてまして、
そういえばRailsのprecompileに圧縮版があったな・・・と思い出しました

アセットパイプライン | Rails ガイド

今まで非圧縮版にアクセスさせていたので、
ガイドの通りnginxを設定し、圧縮版にアクセスさせたところ・・・

・・・ここまで変わりました(; д ) ゚ ゚

ここからモバイルアクセスの点を上げるには、
レンダリングをブロックしているJS/CSSを改善しないといけないらしいです

Remove Render-Blocking JavaScript  |  PageSpeed Insights  |  Google Developers

とはいえ、完全reactの状態では難しく、
ファーストビューだけでもサーバサイドでレンダリングする必要があるのですが、
その問題自体は認識していて、今後の課題ですかね・・・

developers.cyberagent.co.jp

こういうことが「さらっと」(=私の手の届く範囲で)できるといいのですが(´-ω-)

まとめ

  • Redisキャッシュを使えばいいというわけではなく、使い方をちゃんと考える
  • Railsのガイドはちゃんと読む(´・ω・`)
  • 今後の課題:完全reactから一部をサーバサイドに差し戻す

*1: 昔はmemcachedが定番でしたが、Redisでもたいして速度が変わらず、永続化も可能なので、永続化が必要ならRedis、そうでなければRailsのメモリって感じで、memcachedは使ってません(´-ω-)

アンドロイドはアイドルの夢を見るか(恋するハッカソン〜君色に染まるアイドル〜を解いた件)

ということで、8回目のPOHなのですが・・・

paiza.jp

・・・前回あたりから「ゲーム」としてPRしていたり、
今回から会員登録しないとダメだったりと、
そろそろいいかな・・・とも思いまして(´-ω-)

しかも、先月は死ぬほど忙しくて、
普段飲まないエナジードリンクを飲みながら仕事していたレベルで、
こういう遊びをやっている余裕もなくてですね・・・

でも、仕事もやっと一段落して、そもそもそのエナジードリンクは、
以前のPOHで当たったやつだったりしたので、
会員登録くらいはまあいいかな・・・とかΣ(・ω・ノ)ノ

ただ、外部サイト連携で登録しようとしたら全然うまくいかなくて、
結局ID/PASSで登録するはめになったはちょっと・・・
何がおかしかったのは不明です

そんなこんなで先日挑戦しまして、
数時間で(現時点である問題を)全てクリアしました∠( ゚д゚)/

f:id:parrot_studio:20160703110902p:plain

まだロックされているドレスは、
「お仕事」(イベントシーン)をあと20回くらい見ないと解放されないやつで、
面倒なので放置しています( ゚Д゚)y─~~

今回もほとんどは楽勝だったのですが、
水着問題はパフォーマンス的にやや難易度が高め(雑に書くと何秒もかかる)で、
浴衣問題と制服問題がBOSS・・・という感じでしょうか
(それでも初期の頃の問題に比べれば瞬殺のレベルですが)

一応、全部のコードがこちらに(´・ω・)っ

https://gist.github.com/parrot-studio/d657f61ce60969685995b6e7f22bf119

ということで、今回は浴衣問題と制服問題の話だけ書いていきます

浴衣問題

冷蔵庫の電気料金に関する問題です

設定温度より高いと2円/h、設定温度で維持できるなら1円/h
1日に何度か開け閉めすると、そのタイミングで温度が上昇
1日で電気料金はいくら・・・という問題です

この、「状態が変化する」系の問題は、
クロージャーの出番でございます(`・ω・´)

# クロージャー生成
count = 0
counter = lambda do
  count += 1
end

# クロージャーを呼び出す
counter.call
puts count # => 1
counter.call
puts count # => 2
3.times { counter.call }
puts count # => 5

クロージャー(今回の場合はRubyのProcオブジェクト*1)は、
作られた時点での外部環境(countという変数)を参照しているので、
こういうことができるわけですね

今回の問題はこれを使えば簡単で、
クロージャーの中に問題の仕様をそのまま織り込んでしまい、
1時間ごとにcallするだけで終わりです

与えられているのは開け閉めする時刻なので、
その時刻まで状態を進めて、開け閉めした時点で温度を加算して、
また次まで進めて・・・というわけです

これを真面目に解こうとすると結構面倒だと思いますが、
仕様をそのまま書けば解けてしまうというのが面白いですね(`・ω・´) b

制服問題

ざっくり言うと、「52人で大富豪をやって、全員の順位を決めろ」という問題です
先ほどの浴衣問題が「業務系にありがちな問題」だとすれば、
こちらは「ゲーム系にありがちな問題」という感じでしょうか

まず面倒なのが、「カードの強弱」です
大富豪は3が一番弱く、10->J->Q->K->A->2という感じで強さが決まっており、
これを毎回比較するのはコストが高いです(´-ω-)

なので、データを読み込む時点で、3を0、4を1・・・2を12というように、
全部強さの順に数値化してしまい、単純な大小比較に持ち込みました

でもこれはたいした話ではなく、最大の問題は「場が流れる条件判定」です

問題通りだと、ある人がカードを出したあと、その人の次まで誰も出せなければ、
次の人が自分の手札を出せる・・・となっていますが、
そのまんま実装したら明らかに計算が無駄です(´・ω・`)

要は、「その人が出したカードが現時点で最強なら場を流す」ということなのですが、
そうなると「その人の出したカードが最強かどうかをどう判定するか?」という問題が

最初のうちは「2」が最強なので、2を出したら場が流れます
しかし、4枚2を出されたら、次の最強はAですし、
刻々と最強のカードは変わっていくわけです

これをどうしたものかな・・・と思ったのですが、
わりとシンプルに「使ったカードを排除していく」という手段をとりました
(コード内のuse!メソッド)

あとは「場が流れた」ことを-1で表現して、次の人が確実に出せるように単純化したとか、
ランキングを配列で表現したとか、いつものレベルの最適化でして、
52枚と枚数が決まっているため、計算量もほぼ一定で、パフォーマンス的にも難しくありません

まとめ

ということで、現時点ではこの2問がやや難しいといった感じで、
他は効率よく書けるかはともかくとして、「書けない」ということはないはずです

見た感じあと2問、まだ解放されていない問題があるようなので、
これがさっきの2問より難しいようなら、またこの記事に追記します...φ(・ω・`)

*1: 突っ込まれそうなので先に書いておきますが、厳密にいうと、Rubyラムダ式とProcオブジェクトは振る舞いが違います ただ、私は普段、その違いを意識しなければならないような使い方はしてないので、ほぼ同じようなものとして扱っています 重要なのは、「クロージャーという(一般的な)概念がこういう場合に使える」ということです

Rails4.2からRails5.0(RC1)に移行する際に修正したポイント

昨年の秋あたりから、お仕事の関係で監視していたRailsの開発状況ですが、
お仕事が関係なくなっても、なんとなく毎日チェックしておりまして

どうしてもβを採用するのは怖いので、(仕様が固まる)RC版を待っていたところ、
先日RC版がリリースされました(`・ω・´)

Riding Rails: This Week In Rails 💯: RailsConf recap & Rails 5.0 RC 1 is out!

ということで、手持ちの「チェンクロパーティーシミュレーター」を、
Rails4.2からRails5.0RC1に移行したわけです

ccpts.parrot-studio.com

移行する際に書き換えたコードがこれになりますが・・・

Rails5(RC1)に移行 / 入手先検索のバグを修正 / viewのコードを修正 / データの誤りを修正 · parrot-studio/cc-pt-viewer@9f3e715 · GitHub

・・・全体を見直したことによるバグfixも含むとはいえ、
なんとなくどこが変わったのかはつかめるんじゃないかと

以下、引っかかった点や変更点をざっくりと...φ(・ω・`)

modelをApplicationRecordからの継承に変更

以前は「ActiveRecord::Base」から直に継承していたmodelですが、
Rails5から「ApplicationRecord」というアプリ層クラスを経由するようになりました
この変更はとてもいいので、Rails4.2で進めている今のお仕事でも取り込んでいます(`・ω・´) b *1

例えば、複数のmodelを触るトランザクションを張る場合に、以前だと・・・

# Railsのクラスを直に触っている
ActiveRecord::Base.transaction do
  # なにか更新処理
end

# これが嫌なら、自分のクラスを経由するけど、なにか気持ち悪い(´-ω-)
Arcana.transaction do
  # 実際はArcana以外のmodelも更新している
end

・・・こんな感じで書くしかありませんでした

しかし、一つクラスをはさんだことで・・・

ApplicationRecord.transaction do
  # 明確な親クラスでトランザクションを使える
end

・・・こう書くことができて、とてもすっきりしますヽ(`・ω・´)ノ

トランザクション以外にも、アプリ全体に処理を仕込みたい場合、
全部「ApplicationRecord」に書けばいいので、ルールとしてわかりやすいです

Relationの仕様変更にはまる

Railsのバージョンアップをする際、だいたい鬼門になるのはActiveRecordです
今回も見事に仕様変更の罠を踏み抜きました(lll゚Д゚)

チェンクロのアルカナ情報を表すクラスとしてArcanaクラスを用意して、
その下に細かい情報クラスをぶら下げておりました

# Rails4.2のコード
class Arcana < ActiveRecord::Base
  # 中略
  belongs_to :first_skill,    class_name: 'Skill'
  belongs_to :second_skill,   class_name: 'Skill'
  belongs_to :third_skill,    class_name: 'Skill'
  belongs_to :first_ability,  class_name: 'Ability'
  belongs_to :second_ability, class_name: 'Ability'
  belongs_to :weapon_ability, class_name: 'Ability'
  # 後略
end

しかし、Rails5に移行したら、データのimport処理で引っかかりまくりまして、
吐き出されたエラーメッセージを読んだところ、
「結びついたデータがないぞ(#゚Д゚)」といったことが書いてありました

以前はこれで動いていたわけで、なにかおかしい・・・と思ったところ、
Relationの存在チェックするバリデーションが、Rails5からデフォルトで有効になっていたようです

関連を定義したのだから、その先の情報もチェックするのが当然・・・という思想はわかるのですが、
全てのアルカナが二つ以上スキルを持っているわけでもなく、
アビリティに至っては持ってないケースもあるので、これは困ります(´-ω-)

そこで、このようにチェックを無効化する必要があったのです

# Rails5のコード
class Arcana < ApplicationRecord
  # 中略
  belongs_to :first_skill,    class_name: 'Skill'
  belongs_to :second_skill,   class_name: 'Skill',   optional: true
  belongs_to :third_skill,    class_name: 'Skill',   optional: true
  belongs_to :first_ability,  class_name: 'Ability', optional: true
  belongs_to :second_ability, class_name: 'Ability', optional: true
  belongs_to :weapon_ability, class_name: 'Ability', optional: true
  # 後略
end

これで以前と同じ動作になります(`・ω・´)
(first_skillだけは必ず持っているはずなので、逆にチェックを増やした状態になってます)

schema.rbがすっきり&MariaDB関連のパッチが採用

移行するついでに、migrationを統合してしまおうと思いまして、
schema.rbの内容をそのままコピペしたmigrationを作ろうとしたのですが、
だいぶいろいろ変わっておりました

ActiveRecord::Schema.define(version: 20160507003347) do

  create_table "abilities", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin" do |t|
    t.string   "name",        limit: 100, null: false
    t.string   "explanation", limit: 500
    t.string   "weapon_name", limit: 100
    t.datetime "created_at",              null: false
    t.datetime "updated_at",              null: false
    t.index ["name"], name: "index_abilities_on_name", unique: true, using: :btree
  end

# ...

end

まず、create_tableのオプションに、MariaDBMySQL)固有のオプション情報が付加されています
あと、indexの生成が「add_index」ではなく、「t.index」という形でcreate_tableに統合されています
後者はとてもいいですね(`・ω・´) b

しかも、migration関連の処理がえらい速くなりました
単にmigrationを統合したから、という感じではない速度です

デフォルトAPサーバがpumaに変更されたが・・・

ActionCableが追加され、WebSocketが採用されたために、
Webrickからpumaに変更されたのですが、
これがいろいろ問題を引き起こしているようで・・・(´-ω-)

configレベルで誤りがあって起動しないって場合に、
Webrickなら例外を吐いて止まってくれるのですが、
pumaだと落ちずにずっとフリーズしたままで、他のコンソールからkillしないといけません

特にコンソールに例外を吐いてくれないのが困ります
どこを直せばいいのかわかりませんし(´・ω・`)

他にもreload周りのissueが挙がっているようですし、
まだまだ不安定な感じがしますね・・・
まあ、本番で使うわけではないですし、コードの本体をいじる際には問題ないのですが

ArelでOR文の書き方変更

Rails5より前だと、ActiveRecordでOR文を書く際、
やや面倒な書き方をしなければいけませんでした(´-ω-)

  def skill_search(category, cost, sub, ef)
    return [] if (category.blank? && cost.blank?)

    arel = SkillEffect.all
    arel.where!(category: category) unless category.blank?
    arel.where!(subcategory: sub) unless sub.blank?
    arel = arel.joins(:skill).where(skills: { cost: cost }) unless cost.blank?
      unless ef.blank?
        efs = [ef].flatten.uniq.compact
        arel.where!(
          SkillEffect.where(
            subeffect1: efs
          ).where(
            subeffect2: efs
          ).where(
            subeffect3: efs
          ).where(
            subeffect4: efs
          ).where(
            subeffect5: efs
          ).where_values.reduce(:or)
       )
    end

    arel.pluck(:skill_id)
  end

「where_values.reduce(:or)」って書き方は美しくないので、
「or(条件)」という書き方が採用されております(`・ω・´) b

そこまではいいのですが、ANDとORが混在する場合のルールがいまいちよくわからなくて、
whereとorの順番を変えるだけで、いろいろ変わってしまいまして・・・

先にwhereを書いてからorを書くと、whareの条件と混線してしまうので、
orを先に書いてしまってからwhereを書くことでなんとか

  def skill_search(category, cost, sub, ef)
    return [] if (category.blank? && cost.blank?)

    arel = SkillEffect.all
    # 先にOR条件を構築
    unless ef.blank?
      efs = [ef].flatten.uniq.compact
      arel = arel.where(subeffect1: efs)
        .or(SkillEffect.where(subeffect2: efs))
        .or(SkillEffect.where(subeffect3: efs))
        .or(SkillEffect.where(subeffect4: efs))
        .or(SkillEffect.where(subeffect5: efs))
    end

    # そのあとAND条件を付加
    arel = arel.where(category: category) unless category.blank?
    arel = arel.where(subcategory: sub) unless sub.blank?
    arel = arel.joins(:skill).where(skills: { cost: cost }) unless cost.blank?
    arel.pluck(:skill_id)
  end

このあたりは生成されるSQLを眺めて慎重に進めるしかないのですが、
やはりドキュメントが欲しいですね(´-ω-)

キャッシュの制御がフレームワーク

処理効率化のため、Rails.cacheを使うってのはよくあるはなしですが、
開発環境では邪魔だし、かといってテストもできないと困るので、
以前は自前のconfigにON/OFFフラグを記述して制御していました

   def with_cache(name, &b)
      return unless (name && b)
      # configの情報を見て利用の有無を制御
      return b.call unless ServerSettings.use_cache?
      Rails.cache.fetch(name, &b)
   end

これがRails5では不要になりまして、
「tmp/caching-dev.txt」の有無で制御できるようになってます
(実際の操作は「rails dev:cache」か「rake dev:cache」するだけです)

# config/environments/development.rb
  # Enable/disable caching. By default caching is disabled.
  if Rails.root.join('tmp/caching-dev.txt').exist?
    config.action_controller.perform_caching = true

    config.cache_store = :memory_store
    config.public_file_server.headers = {
      'Cache-Control' => 'public, max-age=172800'
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

こういうコードがdevelopment.rbに自動生成されるおかげなので、
他のバージョンでもこれをコピペすれば同じことができます(`・ω・´) b

まとめ

ということでざっくりと修正した点を見てきましたが、
思ったよりも少なかったかな・・・という印象です
まあ、アプリの規模が小さいってのはありますが(´-ω-)

Railsの思想から逸脱したコードを量産しない限りは、
わりと移行しやすいんじゃないかと思いますので、
やはりRailsの基本的な仕組みは理解しておいた方がいいですね

これを書いている時点の最新はRC1のままで、
リリース版になったらまた修正が入るかもしれないので、
その際はまた書いてみます...φ(・ω・`)

*1:rails g model」した後に、いちいちクラス定義を書き換える手間はありますが、それを上回るすっきり感がありますし(`・ω・´)