ぱろっと・すたじお

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

FRP(Functional Reactive Programming)を試した話+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的な問題が解決する・・・はずです