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

ぱろっと・すたじお

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

既存のWebアプリをReactで書き換えた話(+ES6の困った点)

ccpts CoffeeScript JavaScript

本当は1ヶ月前に下書きを書いた記事なのですが、
忙しくて見直す時間がなく、だいぶ遅くなってしまいました(´・ω・`)

その間にバグfixを進め、一応安定動作していますし、
それもふまえた内容に書き換えていく方向で・・・


相変わらずメンテと拡張を続けている
「チェンクロパーティーシミュレーター」(以下「ccpts」)ですが、
先日Reactでほぼ全体を書き換えました(`・ω・´)

ccpts.parrot-studio.com

github.com

ちょうど1年ほど前、私がReactを初めて意識した際に、
一度やろうとしたことはあるのですが、
いろいろな理由*1があって諦めてました

しかし、昨年から仕事でNode.jsを扱うように*2なり、
先日はccptsのロジックをFRPで整理するのに成功したりと、
今ならいけるんじゃないか・・・と思い、今回試してみたわけです

React(+flux)についてはいくらでも素晴らしい解説が見つかりますので、
今回は「すでに動いているものをReact化するまでの段取り」と
「その過程で考えたこと」を書いていきます...φ(・ω・`)

全体の段取り

React化に以前失敗しているのもあり、事前に全体の段取りを立てて臨みました
(というより、その段取りが見えたから今回進めたというか)

まず、react-railsですが、これを使うとjsxのサーバサイドレンダリングが可能になります

github.com

初期の段階ではerbで書かれたHTMLの構造を、ほぼそのままjsxにコピーしていき、
サーバでレンダリングさせることで、
jQuery等から見たときの)HTMLの構造はほぼそのままになります*3

erbをjsxに書き換えただけにはなりますが、初期の段階ではそれでも十分ですし、
サーバでのレンダリングをやめることが最終目標になるわけです

ccptsのメインロジックはブラウザで実装されており、
erbで動的な出力は最低限になってはいますが、
それでもReactを前提にするといろいろいじる必要は出てきます

(余談ですが、元のviewがHTML埋め込みのerbだったからこそ使えた手で、
 haml等を使っていたら全部書き直しでしたね・・・
 別にhamlが悪いわけではなく、結果的に、ですけども)

react-railsのおかげで、部分的に置き換えていくというのが容易になったので、
動的制御のない部品をどんどんjsxに置き換え、
画面で動作確認する・・・というフローが可能になりました

以前は上から作っていったので、終わりが見えずに心が折れましたが、
下から部品単位で置き換えていけば、「動く状態」を維持できるため、
「いつでもリリースできる」という安心感もあります(`・ω・´) *4

同様に、flux等のstate管理フレームワークは考えませんでした
そういった仕組みを導入すると、また上から全部考え直しになるので、
どんな形であれReact化してから考えればいいかな・・・と

(flux実装の一つであるreduxを見て今回のReact化を検討したり、
 ES6の使用を決めたのは事実ですが・・・)

実際の進め方

1. ほぼコピペでjsxにコードを移動

先ほども書いたように、erbにロジックはほぼ入ってなかったので、
ガンガンとコピペしていくだけです( ゚д゚)o彡゚

コピペで引っかかるのは「class="hoge"」を「className」に変えるとか、
「for="piyo"」と「htmlFor」に変えるとか、
「inputやbrタグにも閉じタグが必要」とか、そんなところでしょうか

ほぼ機械的に変換できますし、ChromeのReact拡張を入れておけば、
consoleで丁寧にwarningが出るので、順番に潰していけばOKです

erbでなにか埋め込んでいる箇所は、react_renderの引数にその値を埋め込んで、
jsx側のpropsで受け取るようにしておきます

# LatestInfoはReactのクラスでChangelogはRubyのクラス
# prerender: true によりサーバサイドでレンダリングされる
<%= react_component('LatestInfo', {info: Changelog.latest.as_json}, prerender: true) %>

jsxに移す際に「クラス名」をつけることになるわけですが、
名前をつけるというのはそれが何者であるのかを明示する、ということであり、
これでViewの構造をもう一度見直すことができたのはいい感じでした

ただし、これはあくまで「補助輪」です
この段階ではreact_componentを使いまくりますが、
その後はむしろ、react_componentを使わない=pureなJSに修正していくわけです

2. 動的フォームでstateを使用

これで全体をいったんjsxに書き換えたのですが、
問題になるのが動的に構造を作っている箇所です

ccptsの場合、検索フォームがその一つで、
大項目を選ぶと、小項目がそれに合わせて作られる、という仕組みになってます
(アビリティやスキル検索のところなど)

しかし、jQueryでイベントを発火させても、
書き換えたDOM構造をまたReactが上書きしてしまうのですΣ(゚Д゚)ガーン *5

そこで、この検索フォームをReact化の第一歩として、
少しずつロジックを実装していくことに

最初はスキルとかアビリティとかの単位で分離し、
その中でstateを管理する・・・という形でいったん実装したのですが、
それで困るケースが二つ出てきまして

一つが「条件のリセット」です

それぞれの項目を初期状態にするわけですが、
値を各component単位で管理しているので、
どうやってresetしてもらうか、という問題が

もう一つが「初期クエリがある場合」です

ccptsのデータベースモードは、検索結果を共有する仕組みがあり、
表示時点で指定された検索クエリをformに反映させないといけません

リセットは「空にする」ことで対応できますが、
初期状態は不定なので、単純には行きません(´-ω-)

そこで、Reactの設計らしく、親である検索フォームで全stateを管理し、
子である各フォームにpropとして伝える、という仕組みを採用しました

このとき、各フォームごとにハンドラを親で定義して、
子に引き渡す・・・というのが面倒だったので、
親でstream(Bacon.Bus)を用意しておき、子は変更された値をpushする仕組みにしました

// from children
this.notifier = new Bacon.Bus()
this.notifier.onValue((s) => {
  // 子から"{job: 'F'}"みたいにpushしてもらう
  this.setState(s)
})

ここまで書いて気づいたのですが、この「stateの塊」って、
実はほぼ「検索クエリ」なんですよね

FRP化した際、検索ボタンを起点にして、クエリを構成し、
APIに投げて結果からviewを生成・・・というstreamが定義されてました

ということは、検索フォームのstateを検索のstreamに乗せてやれば、
そのまま既存のstreamから検索が動くってことなのです
「クエリの構成」という部分が、フォームのparseではなく、stateで終わるってだけで

逆に、外部からのクエリを検索フォームのstateに反映させれば、
フォームの状態を自由に変えることも可能です

つまり、「検索フォーム」というcomponentを、
「クエリを受け取る口」と「クエリを吐き出す口」を持った、
独立した部品としてみなすことが可能です(`・ω・´)

flux実装であるreduxは、アプリ全体で一つのstateを管理する仕組みのはずですが、
FRPから入った私にとっては、「通知用streamが存在する独立した部品」の方が、
概念としてしっくりきたわけです

RxJSとReactを組み合わせたフレームワークに、
Cycle.jsというのがありますが、それに近い形です

cycle.js.org

ここまで徹底してはいませんが、考え方は近いです
「何かあったらstreamに流すのでよろしく(`・ω・´)ノ」というのが、
まさにObservableな設計であり、個々の関係を切り離す鍵になってます

3. react-bootstrapの部品を使う

これで完全に「つかんだ」ので、
他の動的な部分ももりもりと進めていこうと思ったのですが、
次に引っかかったのがmodalの扱いです

元々jQuery経由でbootstrapのJSが制御してましたが、
modalの中に状態がある、というケースがありまして、
検索フォーム同様、そこがReactだとうまくいかないわけです(´-ω-)

そこで、このタイミングで「react-bootstrap」を投入することに

React-Bootstrap

リファレンスを見てもらえばわかりますが、ModalやPaginationなど、
動的な処理をしてくれるcomponentがたくさんありまして、
これでだいぶ構造が整理されました(`・ω・´)

Paginationなんか自前で書いてましたが、
この部品を使えば値を渡すだけですからね・・・
以前はここまで整備されていなかったと思います

一方、無理に全ての部品を置き換える必要はなく、
ほとんどはdiv+classでけりがつきますし、
部品にpropが足りないので、あえてdiv+classのままにしたところもあります

4. データベースモードがReact化

あとは、何か通知が必要ならstreamを定義し、
streamを整理していくうちに統合化され・・・と、
開発を進めていったところ、データベースモードがほぼReact化されました

データベースモード : Get our light! - チェンクロ パーティーシミュレーター

そもそも「データベースモード」は、「PT編集モード」の検索部分を切り出し、
見せ方だけ変えたものなので、これで作られた部品は、
ほぼそのままPT編集モードに使いまわせます
(まさに「コンポーネント指向」かと)

この時点で、「いつでもリリース可能」という状態を捨てて、
データベースモードの部品から共通部分を取り出しつつ、
PT編集モードをトップダウンで作っていくモードに

5. jQueryとの連携

Reactの話を見ていると、「jQueryを捨てる」的な話がよく出てきますが、
ccptsに関していえばそれは不可能です

なぜなら、PT編集モードはドラッグアンドドロップが必須で、
それらを簡単に使える仕組みがReactにはまだないからです(´・ω・`)

あと、アニメーションに関する仕組みも貧弱すぎます
一応、react-bootstrapにはFadeの部品がありましたが、
いまいち使い方がわからないし、jQueryと同じ感覚でもないです

そこで、どうやってReactのVirtual DOMと、
jQueryのDOMを連携させるか・・・ですが、
refという仕組みでDOMを生で抜き出すことが可能です

<div className={`${a.jobClass} summary-size arcana`}
  ref={(div) => {
    // このdivタグにドラッグのハンドラをセット
    this._div = div
    this.setDraggable(a.jobCode, this.props.code)
}}>

同じように、jQueryのfadeInやhideもセットできるので、
Fadeのcomponentは捨てまして、全部jQueryに切り替えました

ただし、「どのタイミングでアニメーションを適用するか?」は、
Reactのライフサイクルを把握してないとうまくいきません(´・ω・`)

Component Specs and Lifecycle | React

「componentDidMountのとき」だとか、
「setStateが完了したあとのcallback」だとか、
適切な見極めが必要になります

あと、ブラウザや環境依存でうまく動かないケースもあります
Safariだけ挙動がおかしいケースがあったりで、だいぶ悩みました*6

それでも、Reactの仕組みで自前のアニメーションを適用するよりはるかに楽です
Reactの仕組みは難しすぎ、私には無理です(´-ω-)

Animation | React


6. ES6の困った点

jsxのコードを全部ES6で書いたので、
結果的にViewのコードはCoffeeScriptからES6に変わったのですが、
modelや自前のライブラリはCoffeeScriptのままです

一度は全部書き直そうと思ったのですが、
同じロジックのはずなのに動作しないケースがあったりと、
謎が多かったので戻しました

動いている部品を無理に書き換える必要はないわけですし
Viewは作り直しになったから変える動機があったのであって

そもそも、CoffeeScriptと比較して、ES6は以下のような困った点があります

後置ifが使えない

「unlessが書けない」というのも困るのですが、
それ以上に困るのがこれです

普段Rubyを書くときもそうですが、
私はメソッドの頭にガード条件をよく書きます

someMethod = (a) ->
  return unless a # aが存在しなかったらreturn
  # aを使う処理

これがES6だとこうなります

someMethod(a) {
  if (!a) {
    return // aが存在しなかったらreturn
  }
  // aを使う処理
}

一つくらいのガード条件ならいいのですが、
複数並ぶとさすがにうっとうしくなってきますщ(゚Д゚щ)

とはいえ、これだけでES6からCoffeeScriptに戻すってのは、
現在の状況を考えるとあり得ないのですが、最大の問題は次です

クラスのプロパティも定数も定義できない

これに関してはReactの開発側も困っているようですが、
ES6のclassで定義できるのは「メソッド」だけで、
「プロパティ」や「定数」や「privateな変数」が書けないのですΣ(゚Д゚)ガーン

RubyActiveRecordのように、
Reactでもpropsの型をチェックする仕組みがあるのですが、
当然、classで共通なので、classのプロパティとして定義したいわけです

しかし、ES6ではそのような記述が許されていないため、
classを定義したあと、classに代入するという、
美しくない方法を使うしかありません(´-ω-)

もっとひどいのが定数です
見かけ上、「定数のように見えるもの」は書けます

static get hoge() {
  return _hoge // これをどこに定義する??
}

「get / set」と使うと、かっこなしでのメソッド呼び出しや代入が可能ですが、
「値を格納する変数」はどこに書けばいいのか・・・というのが問題です

ただの定数ならclassの後で代入でもいいですが、
その値をclass内のメソッドで使いまわしたいとすると、
class定義の外で変数を定義しないといけません

でも、それってグローバル環境を汚染しているわけで、
「何のためのclassなのよ(ノ゚Д゚)ノ彡┻━┻」という気分に

他でもできないならいいのですが、CoffeeScriptはもちろん、
TypeScriptでもできるし、ES7には入ってるわけですからね・・・

結局、この仕様が気に入らず、babelでの変換もいまいちうまくいかなかったので、
今回はmodel等はそのままにしています

7. 最終調整とReact化の弊害

いろいろと壁はありつつも、なんとか大枠での実装が完了し、
Viewの実装はほぼ完了し、関連ライブラリも全てnpmに移行しました

そこで、テスト環境で実機での確認に移ったのですが、
ブラウザ依存の問題やバグは別として、以下のような問題が発覚しました

・precompileがめちゃくちゃ遅い

デプロイ時に「rake assets:precompile」を呼ぶ際、
CoffeeScriptとは比較にならないレベルで時間がかかります(lll゚Д゚)

Reactのjsxはもちろん、ES6も変換しないといけないし、
Viewで使うJSのファイル数が跳ね上がったから仕方ないのですが、
いくら本番VPSのCPUが弱いからといって、数分待たされるのは怖いです

まあ、precompileが完了してしまえば、
実際の呼び出しで(変換により)待たされることはないので、
viewのコードを変更してデプロイした場合に限られるのですが

頻繁にデプロイするとか、規模が膨大の場合は、
ローカルでprecompileしておいて、
出来上がったファイルをgitで管理する・・・なんても必要かもしれません

・JSの読み込みが遅い

precompileを経由し、全て結合したファイルにアクセスするとはいえ、
JSのファイルサイズは大きく、ロードに結構かかります(lll゚Д゚)

一度キャッシュされれば許容範囲ではあるものの、
それでも以前の単純なviewに比べ、待たされる感があります

以前は枠をサーバでレンダリングし、
中身を非同期通信で呼び出す仕組みだったのでまだ良かったのですが、
Reactだとアプリ本体が全体を描画するので、何も表示されない時間があるのです

JSの読み出しと解釈に時間がかかるって問題を、
JSの文脈(jQuery等)で解決ってのは当然できません
なんとかして別の手段でごまかす必要があります

そこで、サーバ側で「loading」って画面を描画しておき、
Reactの描画が完了した時点で消す、という手段を使いました
これだけでも「何かしてるんだな・・・」という感じが出ます(`・ω・´)

これからのお話

そんなこんなで、やっと安定して稼働し始めたccptsのReact版ですが、
最後にやってみた感想とか今後の話などを...φ(・ω・`)

仕事に役立つのか?

少なくとも、今の環境では使わないかな・・・と思ってます

基本、私の仕事はAPIとかサーバサイドの構築であり、
今回の主目的はES6とか非同期処理の記述に慣れるのが目的でした
例えば、システムの管理画面にReactって話は高コストすぎるかと(´-ω-)

もちろん、「未来の最適解」を「プライベートで」模索しておくことに大きな意味があり、
間接的には仕事に反映はされていくはずではあります
少なくとも、仕事でNode.jsを書く際にES6を使おうとは思いました

・・・まあ、CoffeeScriptの方が気楽なんですけどねΣ(・ω・ノ)ノ

reduxをどうするか?

fluxというか、Reactと組み合わせて使うフレームワークとして、
reduxが話題になっており、当初は私の最終目的もそれであり、
だからこそES6を試した・・・という事情はあります

github.com

ただですね・・・FRPとの組み合わせで実装してみると、
「componentの入出力だけお見せするので、好きな時に通知をください(`・ω・´)ノ」の方が、
stateを一箇所で集めるよりも、独立性が高まっている感じがして、しっくりくるんですよね

むしろ、検討するならCycle.jsかな・・・とも思いますが、
そこまでくるともはやReactですらないので、やりすぎかなと(´-ω-)

react_on_rails

完成させてから気づいたのですが、こういう仕組みがあるようです

github.com

クライアント部分を独立した形で開発し、
コンパイルしたjsをassetsに配置するという一連の流れを、
うまいことフレームワークとして自動化してくれるというものっぽいです
(最近よく議論されている方式のフレームワーク化ですね)

おそらく便利だと思いますが、あまり情報が出てないのと、
あまりにフルスタックすぎて、知らない人が一から触ると混乱しそうです
慣れた人がショートカットするにはよさげ



そんなこんなで、自分のメモという意味もこめてだいぶ長々と書いてきましたが、
お仕事でがっつりRailsのお仕事に戻ったのもあり、
しばらくはccptsのview周りのいじることはないかな・・・と
(もちろん、機能追加は別として、フレームワーク的な意味で)

次はAPIをRails5に書き換える予定ですが、
RC1はまだですかね・・・(´・ω・`)

*1: 私自身がJSに慣れてない、react-bootstrapが今ほど整備されていない・・・等

*2: これを書いた時点では扱う予定でしたが、現在のお仕事だと使わない予定ですΣ(゚Д゚)ガーン まあ、また別なところで使うとは思いますが・・・

*3: より厳密に言うと、専用のdata属性付きタグが出力され、react_ujs.jsがそれをフックしてreactのcomponentに置き換えるという仕組みなので、静的なHTMLが直に出力されるわけではありません・・・が、jQueryイベントハンドラをセットする側からすると、ほぼ同じに見える=既存の書き換え不要、というだけです 詳しくはreact-railsのREADME参照

*4: まさにリファクタリングの基本かと

*5: jQueryが「本物のDOM」をいじる -> Reactの仮想DOMはそのままなので、reactが差分を検知 -> 本物のDOMが上書きされる・・・という感じだと思いますが、あってますかね?

*6: jQueryのshowのあと、テーブルが再描画されないという問題だったので、streamに「再描画」メッセージを流して、同じstateで再描画させる手段で解決しました(`・ω・´)

第7の恋愛SLG(「プログラミングで彼女をつくる」を解いた件)

Ruby POH

ふと、セブンスドラゴン3が終盤で止まっているな・・・と思い出しましたが、
とにかく今回の「POH7」は恋愛SLG仕立てだそうでΣ(・ω・ノ)ノ

paiza.jp

まあ、要するに問題を解くとアイテムがGETできて、着せ替えも可能ってだけなのですが、
やっぱり見せ方は大事ですからね・・・(´-ω-)

そんなわけで、今回は問題が多そうだったので、
計画的にクリアしようと初日からざっと問題を確認したところ・・・

f:id:parrot_studio:20151219230420p:plain

・・・1日で終わりましたΣ(゚Д゚)ガーン
(上の画像には追加問題2問を含む)

最下段の3問が高難易度で、後は(プロのプログラマなら)即座に書き下せる難易度ですので、
その3問の話だけ考えたことを書いていきます

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

https://gist.github.com/parrot-studio/96c758f393806a7df6ac

ついでに、過去の問題を解いた記事はこちら

parrot.hatenadiary.jp

parrot.hatenadiary.jp


メガネ

すぐに問題は理解できると思います
ただ、どう「効率よく計算するか?」が難しいのです(´-ω-)

単純に考えると、それぞれの盤面を配列(できれば1次元の)に保持し、
座標を動かしながら、値が一致するかを「各座標で」「順番に」比較していけばいいのですが、
これだと比較にO(m^2)かかるので、おそらく時間切れでpassできませんΣ(゚Д゚)ガーン

これと類似の「2次元の盤面をparseする問題」がPOH2で出ており、
これを解くのに1週間以上かかったわけですが・・・

parrot.hatenadiary.jp

・・・もちろん、今回の問題もこれの応用でいけます

盤面の一行を「0b0010=2」のように捉えれば、
盤面はそれぞれ「n個の数値の配列」と「m個の数値の配列」と考えられます

あとは、座標を動かして比較するのに必要なところを抜き出せばOKです

# 例として、「0 1 1 0」に「0 1 1」が含まれるかを調べる

# 3桁分のマスクを作る
mask = ('1'*m).to_i(2) # => 0b0111

# 目的の値とand演算する
t = 0b0110 & mask # => 0b0110

# 比較
t == 0b0011 # => false

# maskを一桁ずらす
mask = mask << 1 #=> 0b1110

# 目的の値とand演算する
t = 0b0110 & mask # => 0b0110

# ずらした桁を戻す
t = t << 1 # => 0b0011

# 比較
t == 0b0011 # => true

実際のコードは、これを座標をずらしながら、
複数行=m個の数値の比較でやっているだけです

https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-ex1_glasses-rb

この方法だと「たかだかm個の数値同士の比較」に落とし込めるので、
だいぶ高速化できますヽ(`・ω・´)ノ

ただまあ、これは以前1週間以上悩んだからこその解法であり、
やはり積み重ねは大事ですね

サンタ服

3つの問題の中では最も簡単な問題です
文章を仕様に落とし込めれば楽勝かと(`・ω・´)

問題をよく読んでいくと、求めるのは「体積」なのですが、
「水平方向=z軸」には切りません
つまり、「最小の"面積"」を求めて、最後に高さをかければ十分です

ナイフは常に「並行」に入ります
なので、単純に「x軸とy軸それぞれで一番小さい幅」をかけたものが答えです

しかし、渡されるのは「位置」であって「幅」ではありません
しかも、渡される順番も適当です

そこで、いったんx軸とy軸ごとに配列に格納し、
ソートした後、隣同士の座標で幅を計算=引き算して、
「一番小さな値」を取り出せばOKです

ここまで問題をかみ砕いてしまえば、書くのはとても簡単です(`・ω・´) b

https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-ex2_santa-rb

ポイントは「端っこの値=0とx(or y)」を配列に入れてしまうことです
そうすることで、特別なif文など不要になり、単純なリスト処理でけりがつきます

水着

一応ラスボスじゃないかとは思いますが・・・
これをクリアすると、海の背景も開放されますし

問題の意味はすぐに理解できますが、
ポイントは「莫大な計算量をどうするか?」です

仕様通りに計算すると、Rubyなら一応ベタに計算できますが、
言語によっては桁あふれになりますし、
Rubyだとしてもパフォーマンスが最悪です(´-ω-)

そこで、どう間引くかが問題になるのですが・・・
正直、自分の中でも確信は持てていません

それでも一応1秒程度で実行できたのがこちらです

https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-ex3_swimwear-rb

基本はただかけ算をしていくだけですが、
「適当な桁数」で数値を補正しています

  1. 数値を文字列にして桁ごとに分割
  2. ひっくり返して頭から0を取り除く
  3. 「適当な桁数」の数字文字列を取り出す
  4. 再びひっくり返し、結合してから数値化

どうせ最後に下の桁の0は全部取り除かれますし、
欲しいのはその上の9桁なので、途中でそれを計算してしまえば、
ある程度の桁数に抑えられるでしょう・・・と

とはいえ、途中の桁数でぴったり9桁にすると、
次の数値をかけた時に必要な桁のところに誤差が来てしまうので、
最初12で実行し、11->10と下げていって、10では止まったので11・・・という

ただ、いろいろ試行錯誤はしたのですが、どうしても1秒を切れません
おそらく「reduce」の処理をもっと最適化するか、
そもそもやり方を変えるか・・・(´-ω-)

最初は毎回間引いていたので、一定回数ごとに抑えたところ、
8秒だったのが1秒まで削れました
つまり、間引く処理が遅いことになります

でも、あまり間引く回数を減らすと、今度は桁が大きくなりすぎて計算が遅くなります
まあ、いろいろやっても1.11秒と1.13秒ばかりなので、
なんらかの限界の可能性はありますが・・・

Rubyに関していえば、今回の問題はおそらくBignumの範囲で計算されているので、
Fixnumの範囲で押さえられないか・・・とか、
そのあたりでしょうね(´・ω・`)


まとめ

ここ数回の難易度に比べると、元の難易度に近くなったかな・・・という気はします
サンタ服の問題はともかく、他2問はこの手の問題をやったことがないと難しいでしょう(´-ω-)

むしろ、このサイトのメインターゲットである、
「転職を考えているエンジニア」の観点だと、
この3問以外の、残りの問題があっさり解けるか・・・の方が大事です

それぞれベタな書き方は可能ですが、面接で効率の良いコードを書けると、
「お、この人はいいかも」と思わせることができます(`・ω・´)
(というか、面接官にその観点がない場合、その会社は怪しいかと・・・)

https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-others-rb

1〜数行で書ける問題ばかりですので、いろいろ試してみてはどうでしょう

FRP(Functional Reactive Programming)を試した話+JS周りのあれこれ

JavaScript FRP CoffeeScript ccpts Node.js

ここのところ、久々に技術調査をがっつりしており、
そのあたりをメモするのがメインの記事になりますので、
いつも以上にまとまりがないと思います(´-ω-)


私はサーバサイドのエンジニアで、Rubyを主に扱っており、
仕事でもほぼRailsを書いていたのですが、
ちょっとした事情によりNode.jsも書くことになりまして*1

今までJavaScriptを主体にしたコードを書いたことがなく、
プロトタイプ*2をいじくり回しているのですが、
やはり「本物」っぽいコードを書いた方がいろいろつかめるわけです

ということで、手持ちの「チェンクロパーティーシミュレーター」(以下ccpts)で、
いろいろ試しております...φ(・ω・`)

ccpts.parrot-studio.com

ということで、以下考えた思考過程のメモなどを


FRP(Functional Reactive Programming)

Node.js・・・というかJavaScriptを本格的に書きはじめて、
最初に引っかかったのはやはりcallbackの多さです
あらゆる処理を非同期で処理するのが前提ですからね(´-ω-) *3

もちろん、有名どころではPromise*4を使うのが定番で、
最初はそれで書いていたのですが、「もっといい手」があるのでは・・・と気になりまして

そんな時、ちょうど流れてきたのが「Rx」の話でございます*5

ReactiveX

ninjinkun.hatenablog.com

Rx自体は別にJavaScriptに限った話ではなく、不完全ながらRubyにもあるわけですが、
フロントエンドで使われることが多いので、本家のC#JavaScriptがやはり目立ちます

でまあ、先ほどの記事を読むとなんとなくわかる・・・というか、
わかった気になると思いますが、要するにRxのようなFRPとは・・・

 「非同期処理と同期処理を一つのストリームで扱えるもの」

・・・であり、Rubyの文脈で雑に書くならば・・・

 「イベントも扱えるようになったEnumerator」

・・・と考えればだいたいあってます

そもそも、プログラムを書く側からすると、非同期処理だろうと同期処理だろうと、
「渡した値に対応する値が返ってくること」にしか「興味がない」わけです
(まさに「関数」です)

理想的には・・・

 「イベントが発生」->「必要な値に生成」->「APIに投げる(非同期)」
->「結果を得る」->「画面を描く」

・・・という流れができればいいわけです(`・ω・´)

とはいえ、わかった気になっても、実際に書こうとすると、
正直全く書けませんで・・・

そこで、まずは「callback」と「Promise」を排除することを目的に、
ccptsを書き直し始めました

当初、RxJSで書いてみたのですが、リリースしようとしたところで、
妙に動作が遅くなる問題が発生し、JavaScriptに特化した「Bacon.js」に切り替えました

baconjs.github.io

Rx(JS)の方が「本質的」ですが、Bacon.jsの方が「実用的」なので、
比較的楽に書けました

例えばこんな感じで(´・ω・)っ

# Searcher

@search: (params, url) ->
  params ?= {}
  params.ver = ver
  result = Bacon.fromPromise($.getJSON(url, params)) # AjaxのPromiseをStreamに変換
  result.onError (err) -> $("#error-area").show()
  result

@searchArcanas: (query) ->
  if cached # キャッシュがあったら
    as = ... # キャッシュから読み込む処理
    return Bacon.once(as) # 値を直に返す

  result = @search(query.params(), searchUrl)
  result.flatMap (data) ->
    as = ... # APIから取得して返す
    Bacon.once(as)

# Viewer
Searcher.searchArcanas(query)
  .onValue (as) ->
    pager = createPager(as)
    replaceTargetArea() # 結果のrender処理

https://github.com/parrot-studio/cc-pt-viewer/commit/28793357cac2f08396240fb2104fa2dac615fc45

キャッシュを返す時は同期的な処理ですが、
APIを経由する時は非同期処理です

それを一つのオブジェクトで包んで返すことで、
受け取る先ではどのような経緯でデータがやってきたのか、
意識しなくても書けるわけです(`・ω・´)

これでだいぶ慣れてきたので、イベントからrenderまでを本格的にStreamでつないでみることに

# 検索ボタンを押したとき、queryを構築するStream
queryFromSearch = $(".search")
  .asEventStream('click')
  .doAction('.preventDefault')
  .doAction -> $("#search-modal").modal('hide')
  .map -> Query.build()

# 各queryのStreamを合流させる
queryStream = queryFromSearch # 検索ボタンQuery
  .merge queryfromQueryLog # 検索ログからきたQuery
  .merge queryFromInit # 初期状態

# queryでAPIを叩いて結果を得る
querySearchStream = queryStream
  .map (q) -> (if q.isEmpty() then recentQuery else q)
  .flatMap (query) -> searchTargets(query)

# 他の方法で得たリストのStreamを合流
searchStream = querySearchStream
  .merge favSearchStream
  .merge arcanaNameSearchStream

# 検索結果からページ送りオブジェクト生成
searchResult = searchStream
  .flatMap (as) -> createPager(as, pagerSize)

# このプロパティを各Viewでsubscribeして描画
@targetArcanas = searchResult # 検索結果
  .merge prevPageStream # 前のページをクリック
  .merge nextPageStream # 次のページをクリック
  .merge jumpPageStream # ページ番号をクリック
  .map -> pager.get() # pagerから描画する部分だけ渡す

まあ具体的なコードは正直分からなくてOKなのですが、
重要なのは「Streamの切り貼りだけで処理が書ける」というところです

ところどころ非同期処理があったとしても、
全て「値を渡して値を得る」という形をしていることが大事なのです(`・ω・´)

これを構築したおかげで、インクリメンタルサーチも簡単に追加できました

arcanaNameStream = $arcanaName # 名前入力フォーム
  .asEventStream('keyup change') # イベント
  .delay(500) # モバイルで日本語変換する場合に対応するため、微妙に読み込みを遅らせる
  .map -> $arcanaName.val() # フォームの値を取得
  .debounce(300) # 0.3s置きにに値を監視
  .skipDuplicates() # 「前と同じ値」ならイベントを却下する

nameSearchStream = arcanaNameStream
  .filter (name) -> name.length > 1 # 2文字以上入力してなければ却下
  .flatMap (name) -> searchName(name) # 検索APIを叩く

https://github.com/parrot-studio/cc-pt-viewer/commit/3be1bbf0aaa78fb9072ae1934bbe66f5a4628f64

普通に考えるとそれぞれとても面倒なはずですが、
このような関数の切り貼りであっさり実装できます∠( ゚д゚)/


しかしこのFRP、実際の業務に持ち込めるかというと、
正直微妙なんですよね・・・

今は検証レベルで私が好き勝手やっているので自由に書けますが、
他の人がこのコードをいじることになったら、果たしていじれるのかと

もちろん、FRPの概念が一般化すれば可能です
しかし、RubyのEnumeratorも必ずしも浸透してない状況*6で、
FRPへのジャンプはあまりにも大きすぎるのではないかと(´-ω-)

回転の早いJavaScript界隈ではありますが、
FRPそのものは「設計思想」であり「概念」なので、
そこは安心なのですが・・・


ES6かCoffeeScriptか、それが問題だ

さて、このようにFRPで記述するにあたり、
CoffeeScriptの書きやすさが非常に際立っております
むしろ、CoffeeScriptでなければテンションが上がらないというか( ゚д゚)o彡゚

しかし、いろいろ調べてみると、CoffeeScriptよりもES6に流れが移りつつあります

html5experts.jp

現時点でES6がまともに動くブラウザはありませんが、
Babelのような「ES6のコードをES5に変換する仕組み」も整備され、
さらにサーバサイドで考えればブラウザの状況は完全に無視できます

babeljs.io

もちろん、CoffeeScriptには多数の利点があり、
「後置ifが書ける」とか「括弧がいらないのでシンプル」とかありますが、
一方でES6は「純正のJSである」という利点があります

さすがにccptsをES6に書き換えるのは手間がかかるなんてものではないので、
仕事で書いたプロトタイプを一部ES6に書き換えたりしていますが、
やっぱりCoffeeScriptに慣れているとしっくりこないんですよね(´-ω-)

でも、CoffeeScriptも知らない人にとってみれば、
ES6もCoffeeScriptも習得コストに大差はなく、
むしろ純正のJSを覚えた方がいろいろ便利なのではないかと・・・

ということで悩んでいたのですが、
とりあえず「関数型っぽいコードが書ければ緩和できるのでは?」と、
こちらのライブラリを試してみることに

lodash.com

こちらはあくまでライブラリであり、
ES6だろうとCoffeeScriptだろうと使えますし、
「やりたいこと」が詰まっていてとても便利です(`・ω・´) b

ccptsでもできるだけlodashを使うようにして、
いざES6に書き換えるとしても、コストを下げられるようにしました

実際に書き換える場合はこちらなどを参考に

Moving to ES6 from CoffeeScript · GitHub


ここからどっちに進むか?

あくまで当初の目的は「サーバサイドJSに関する採用アーキテクチャの検討」であって、
View側のJSは目的ではないので、ここから先は私自身の興味になりますが・・・

ここまでViewのコードを書き換えれば、やっぱり次に気になるのは話題のreactですよね

facebook.github.io

ただ、reactを適用しようとすると、Viewをほぼ一から書き直すはめになり、
過去2回ほど挑戦しようとしたものの、途中で挫折しています(´-ω-)

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

ただ、reactとセットで出てきたfluxの概念*7については、
reactと無関係に適用できる・・・らしいです

せっかくStreamで構造を整理し直したので、そこにfluxの概念を投入しておけば、
残りのreact化もスムーズに進むのではないかと(`・ω・´)

そこで、話題になっているらしい、こちらを適用しようと考えました

github.com

amagitakayosi.hatenablog.com

react自体は「データが更新されたらviewを自動で書き変える仕組み」なのですが、
実際に書いてみるとデータ(state)の管理が面倒でして・・・

その点、reduxは「データを一箇所で管理する」という仕組みになってまして、
これなら後々react化もたやすいのではと考えたのです(`・ω・´)

しかし、ここで問題が
ライブラリ管理で使っているのはbower(実際はbower-rails)という仕組みなのですが、
reduxはbowerに対応していませんΣ(゚Д゚)ガーン

Node.js界隈ではnpmでライブラリをインストールするのが一般化しており、
フロントエンド側もその仕組みに移行しつつあるのです

私がbowerで管理し始めたのが今年の頭なので、
まだ1年経たないうちに再検討させられるあたりが、
JS界隈の回転の速さを感じる部分なのですが、それは置いておいて・・・

Railsと新しい仕組みがまだ試行錯誤段階で、
Railsのsprocketsがよくできすぎているために、
皆が暗黙的に使いすぎていて、移行が文字通り「面倒」なのが問題です(´-ω-)

私もこのあたりは調べ始めたばかりで、有効な回答を持ってないのですが、
そもそもjQuery周りのライブラリは逆にbowerにしかなかったりして、
検討を始めるといろいろ難しいです

(DOMレベルでのjQueryは排除できても、ccptsは「jQuery UI」にがっつり依存しており、
 その代替になる仕組みがreactにはまだなく、
 そもそもreactのライブラリはモバイルに問題があるケースが多いという・・・)

react-railsの仕組みを使ってreactの初期状態をサーバサイドでレンダリングする*8とか、
やりたいことはあるのですが、どうにも私の知識やノウハウが追いつきません(´・ω・`)

とりあえず、仕事で「納得できるJSのコード」を追求しつつ、
見つけた概念をccptsで試す・・・というやり方が続きそうです




・・・以上、完全に私自身のメモ書きでしたΣ(・ω・ノ)ノ

*1: 最初からNode.jsを選んだわけではなく、その選択自体もだいぶ手間取ったのですが、それはまた機会があれば・・・

*2: 最小限の要件を満たしつつ、運用とかもふまえた一式

*3: わずかでもブロックする処理を入れてしまう時点で、Node.jsを使う意味がなくなります(´・ω・`)

*4: jQueryAjax処理も今はPromiseを返しますね

*5: Rxが出始めた初期の頃にいろいろ見た記憶がかすかにあるのですが、当時は「何に使うのか?」がいまいちわからず(´-ω-)

*6: 書けないって人はだいぶ減っていると思いますが、「関数型的なデータの流れで捉える」というのはもう一つ上の概念です

*7: https://github.com/facebook/flux/tree/master/examples/flux-todomvc/

*8: それにより、シングルページアプリケーション(SPA)に存在するSEO的な問題が解決する・・・はずです

その文明はわりと現役です(「女子高生プログラマーの大バトル!〜コボール文明の逆襲〜」を解いた件)

POH Ruby

いやまあ、別にこのBlogはPOHの結果を貼るためのものではなく、
前回から今回までの間にLTをやったりはしていたのですが、
仕事がなかなか忙しくてですね・・・(´-ω-)

さすがに新しい仕事に移って半年だと、
Blogで書けるような新しいネタも少なくて・・・
まあ、LTのネタを書き忘れていたのは単なる怠慢ですが

それはさておき、6回目です

paiza.jp

前回が4月なので、約半年ぶり・・・って、そんなに経ってましたか(lll゚Д゚)

parrot.hatenadiary.jp

4回目くらいで難易度が下がりはじめ、
5回目でパフォーマンスを気にしなくてもいいレベルまで下がってましたが、
今回はさらに下がっていた気がしますΣ(゚Д゚)ガーン

f:id:parrot_studio:20150920103011p:plain

(仕事が忙しくて)着手がだいぶ遅くなり、
今ならもう皆さん解いているはずなので、
今回は答えを本文に書いてしまいます...φ(・ω・`)

緑川つばめの問題

https://paiza.jp/poh/joshibato/tsubame

難易度の表示とか何もなく、実際は「霧島->緑川->六村」の順で解いたのですが、
Blogでは簡単な順に書きます

・・・とはいえ、この1問目はあまりに簡単すぎますΣ(・ω・ノ)ノ

「プロのプログラマ」がこの問題を一瞬で解けない場合、
「貴様、普段全くコードを書いてないどころか、まともに書いたことないな?щ(゚Д゚щ)」
と疑われるレベルでしょう
(逆に、あまりに単純すぎて、何か罠があるのでは・・・と疑ったくらいで)

ただですね・・・「転職支援サイト」で、
このレベルの問題を「最低ライン」として提示しているということ自体、
「プログラムを書けない人」の転職が増えているのかな・・・とも思います(´-ω-)

正直、残すほどのコードではないですが、一応こんな感じで

https://gist.github.com/parrot-studio/8fae30021b17177d4ba4#file-tsubame-rb

六村リオの問題

https://paiza.jp/poh/joshibato/rio

さすがに多少難易度が上がりましたが、ご丁寧にも詳細な計算式が提示されているため、
ただその仕様を実装するだけのお仕事です(´・ω・`)

ただ、「小数」を扱う関係上、数値の取り扱いを間違えると誤差が出ます
私も手抜きしたら通りませんでしたし

https://gist.github.com/parrot-studio/8fae30021b17177d4ba4#file-rio-rb

厳密にいえば、きちっと精度を確認し、
BigDecimalやRationalを検討すべきなのかもしれませんが、
今回はFloatで問題なかったのでこれで

さすがにFloatの方が速いですしね・・・ *1
ただ、誤差で思いもよらぬバグを生み出すこともありますが(´-ω-) *2

霧島京子の問題

https://paiza.jp/poh/joshibato/kirishima

今回のメイン・・・ですが、これも前回よりは簡単な気がします
わりと力技で通ってしまい、アルゴリズム的な小技が不要なので

この問題はとにかく「問題を理解すること」が重要で、
その上で「文章をコードに落とし込めるか?」がポイントであり、
あとはせいぜい効率化を考えるくらいでしょう

ルールに従い、順番に盤面をparseしていって、
ゴールにたどり着けば「Yes」、止まるか外に出たら「No」、
ここまではすぐ思いつくレベルです(効率はともかくとして)

問題は「循環して止まらない場合」です
これを排除できないと、単にparseしていくだけでは終わらなくなります(lll゚Д゚)

要は、「一度通ったところを再度踏むと循環する」ということなので、
通った場所を記憶しておいて、また踏んだら「No」と考えればOKかと

そして効率化ですが、当初は盤面をあらかじめ逆にparseしておき、
「数値が来た時点でO(1)の計算量」的なものを目論んでおりました

ただ、実際は盤面のサイズが大きくなるほど、
無駄な計算が多くなる可能性が高く、コードも複雑化しそうだったので、
単純に「一度計算した結果を覚えておく」程度にしました(´-ω-)

https://gist.github.com/parrot-studio/8fae30021b17177d4ba4#file-kirishima-rb

たぶん、名前のついたもっとうまいアルゴリズムとかあるのだと思いますが、
今回はこれでも0.1秒を切ってますし、気にしない方向で( ゚Д゚)y─~~

まとめ

ということでざっと見てきましたが、
2〜3回目くらいの「TestCase5が通らないのぉぉぉぉっ!(つД`)・゚・。」
みたいな難易度からはすっかり遠ざかった印象です

前回のおまけ問題がそれに近いのかもしれませんが、
あれはあれでガチすぎて手を出す前にあきらめる問題だったので、
「なんとなく手を出したら深淵が待っていた」くらいのが個人的な好みです

まあ、そういう問題を出されると、
一週間くらいはその問題のことで頭がいっぱいになるので、
仕事が進まなくなるのですが・・・Σ(・ω・ノ)ノ

*1: C言語レベルで取り扱えるので

*2: つい先日、Floatの誤差でバグが出まして、原因がわからずに数時間つぶれたこともΣ(゚Д゚)ガーン ロジカルなレベルでは間違いがなく、特定の値が来るとおかしくなるってのは、実にデバッグが難しく・・・

続きを読む

幼なじみと許嫁が修羅場らしいが、それなら両方手に入れればいいじゃない

POH Ruby

ということで、5回目です(`・ω・´)

paiza.jp

いつもだと・・・


問題を読む -> 仕様通りに書く -> サンプル通りの出力になるか確認する ->
いったんサイトで実行してみる -> 途中で落ちる -> リファクタリングしてみる ->
小手先の修正では通らない -> 奇抜な方法を試す -> サンプルの出力すら出なくなる ->
煮詰まって悩む -> いったん寝る -> ヒントになりそうなアルゴリズムを探す ->
いまいちいいのがない -> でも調べるうちになんとなく見える ->
書き直してみる -> あれ、もうちょっとで通るやん? -> 直す -> ∠( ゚д゚)/


・・・というプロセスを経由してクリアしております(´・ω・`)

なので、過去の記事においては、
「どう試行錯誤したか?」をまとめているわけです

parrot.hatenadiary.jp

parrot.hatenadiary.jp

しかし、今回はですね・・・


問題を読む -> 仕様通りに書く-> 先回りして罠を回避する ->
サンプル通りの出力になるか確認する -> いったんサイトで実行してみる -> 通ったΣ(゚Д゚)ガーン


・・・ということで、一発で通ってしまいまして(; д ) ゚ ゚

なので、そもそも今回の問題が(いつもに比べて)簡単という可能性もあります
実際、もっと高難易度な問題は別に用意されてますし

paiza.jp

とはいえ、過去の問題の傾向から、
ここを押さえればいけるというポイントがわかっていたのも事実で、
今回は「何を考えたか?」を中心にいろいろ書いてみます...φ(・ω・`)

なお、私の回答は「おまけ」にて

1問目

見た瞬間解けないと・・・とまでは言いませんが、最近のWeb企業を受ける場合、
このような問題を面接で出される可能性はある*1ため、
できれば5分、最低でも15分くらいで解き方を思いつかないと(゚д゚)マズーです

なお、問題文では「ODD」といってますが、
たいていの言語において、配列は0-originなので、
実際のコードでは「EVEN」というのが罠と言えば罠でしょうか

2問目

「周期性のあるものは割った余りを見ろ」ってミルカさん*2が言ってたΣ(・ω・ノ)ノ

この問題は実際の業務でもありそうな話であり、
効率の良いコードとまではいかなくても、全く書けないというのは問題です

3問目:レナルート

先に解いたのはアイドルの許嫁でしたが、
ぶっちゃけ話をきちっと読んでなかったので、
先に目に入った方を解いたという・・・Σ(゚Д゚)ガーン

Excelのような表にあらかじめ値が入っており、
あとから複数指定される領域を結合した領域に含まれる値の合計、
といった問題でございます

ポイントはもちろん「領域には重複があること」であり、
素直に解くならば、まず「領域がどんな形になるか?」を組み立て、
元の表をparseして領域に含まれるかを判定して足す・・・という感じでしょうか

でも、これだと「領域を組み立てる」ので一度parseし、
「合計を算出する」段階でもう一度parseするわけで、
過去の問題だとこのやり方で100点は無理です(´・ω・`)

「読み込み(gets)しながら問題を解く」というのが理想であり、
「最初の表を読み込む」のは仕方ないにしても、
「領域の読み込みと計算」は同時に進めたいところです

要は、「再度読み込まれても値が加算されない」なら良いわけで、
しかも求められているのは「合計」だけです
なので、「一度読んだ領域を0で上書き」すれば、何度読んでもOKです(`・ω・´)

ついでに、素直にデータ構造を組むとHashとか使いたくなりますが、
これも過去の経験から「Arrayのindexアクセスが速い」とわかっていたので、
データをただの配列で保持し、x/y座標からindexを算出する方法を取っています

あと、きっちり問題を読むと、
表領域に対し指定領域のindexが異様に大きいような気がしたので、
一応足切りを入れていますが、どの程度効果があったかは不明です(´-ω-)

<結果>

https://paiza.jp/poh/enshura-rena-ending/5db8bd8c

3問目:ミナミルート

レナルートは業務寄りの問題なので、実行効率はともかく、
全く書けないってことはないと思うのですが、
ミナミルートは「まんま書く」をやろうとして詰む可能性はあります(lll゚Д゚)

いわゆる「落ちものパズル」の話で、
火のついた爆弾が消えると、上にあった爆弾が「落下する」という条件で、
最終的にどんな盤面になるかを出力せよ、という問題です

状態が「空=0」「普通の爆弾=1」「火のついた爆弾=2」と3つあり、
しかも「落下する」ということは「下に何があるか?」を考えなければならず、
普通に解こうとすると結構悩ましい問題です(´-ω-)

しかし、冷静に「出力結果」を見ていただきたいのです
出力するのは「0か1」であり、「火のついた爆弾=2」はどうせ消えるので、
考えなくていいわけです

しかも、「落下する」と考えるから混乱するわけで、
「落下したあとどうなっているか?」を考えると、
「各列ごとに爆弾がいくつ残るか?」がわかればいいわけです(`・ω・´)

つまり、最初の盤面をparseしながら、「各列ごとに1の数をカウント」し、
出力する時にその値に応じた行を出力していけばいいわけです
結果的に、ミナミルートの方が圧倒的に簡単なコードになります

強いていえば、出力時は「盤面の上から」であり、
「下から積み上げ」ではないので、1か0かの判定が多少混乱しますが、
サンプル出力になるように適当にいじればどうにか・・・Σ(・ω・ノ)ノ

(なお、この問題ではカウンターにHashを使っていて、
 「そこはArrayじゃないのか?щ(゚Д゚щ)」と突っ込みを受けそうですが、
 ええやん、それでも100点だし、直すのめんどいし
 そもそも、普段の業務なら迷わずHash.new(0)にしているのもあって・・・)

<結果>

https://paiza.jp/poh/enshura-minami-ending/75addc18

まとめ

過去の問題で学んだチューニングポイントがこちら(´・ω・)っ

  • parseやループ回数をとにかく減らす
    • 細かなチューニングよりも圧倒的に重要
  • 盤面を保持するデータ構造はできるだけ簡潔に
    • 複雑な構造を使おうとしている時点で、解き方がおかしいと考える
  • 簡単に足切りできそうなら切る
  • 結果さえあっていればいいので、過程はどうでもいい
    • 結果から逆算して、求められている計算手法を予測する

この辺がわかっていたので、今回は一発でしたが、
やはり本命はSP問題の方でしょう

・・・時間がないのでやりませんがΣ(・ω・ノ)ノ

*1: 昨年受けた某社では、ここまでではないにせよ、似たような問題をその場で解かされております(lll゚Д゚)

*2:数学ガール

続きを読む