blog.ryota-ka.me

effect system勉強会でCycle.jsの話をしてきた

もう1ヶ月ちょっと前の話になるが,effect system勉強会Cycle.jsの話をしてきた.

発表資料はこちら.

当日のTwitterの様子をまとめたmomentはこちら.

発表内容のサマリ#

「望ましいWebフロントエンドフレームワークとは,以下の2点の要求に答えることができるものである」との仮定のもと,それらの仮定を満たすものとして天下り的にCycle.jsの紹介をし,実際に仮定を満たしていることを確かめる,という構成をとった.

  • a. 非同期が織り込み済みだと嬉しい
  • b. 種々の副作用を互いに分離できると嬉しい

a. 非同期が織り込み済みだと嬉しい#

interactiveなアプリケーションを作る上で,非同期処理はどうあがいても必要になる.Reduxに非同期処理が織り込まれていないのが未だに不思議で仕方がない.そこでRedux middlewareを導入することになるわけだが,redux-thunkはfunctionがdispatchされてtestableじゃなくて「正気か??」という感じがするし,redux-sagaは,まぁ気持ちはわかるのだが,非同期処理のために複雑なメンタルモデルを要求される*1ところが苦しい.

Cycle.jsはobservable/streamの概念を以って同期処理・非同期処理を統一的に記述するので,非同期処理が必要になった際に迷う余地がない.

一方で「observableは確かにversatileだが,実際ほとんどのケースはasync/awaitで済むでしょ」という思想のもとに作られているRedux middlewareもある.それはそれで,それもそうかも.

とはいえ,「そもそもどのRedux middlewareと一生を添い遂げるのか?」という意思決定を迫られること自体,少しつらい気がしている.

b. 種々の副作用を互いに分離できると嬉しい#

Webフロントエンド・アプリケーションにはとにかく大量の副作用が付いて回る.仮想DOMを元にした画面の(再)描画や,ユーザが起こしたclickinputなどのイベントの取得,HTTPを通じたAPIへのリクエストの送信・レスポンスの受信,アプリケーション自体のステート管理,WebSocketを通じた通信,History APIを用いた履歴管理,document.titleの更新,faviconの変更,visibility APIへの対応,などなど,枚挙に暇がない.

これらをすべてまとめてIOという名を与え,一枚岩のものとして統一的に扱うこともできる.できるのだが,経験則として,それぞれを分離したままにしておいた方が,取り扱いが便利であると感じている.個別に分離しておけば,テスト時にも必要最小限のdependencyを与えれば済むし,シグネチャを見れば,それがどのような特徴を持つ計算であるかをより把握できるからである.

Cycle.jsでは,種々の副作用をeffect的に扱う.副作用の処理をclient-serverモデルとして捉え,clientたるcomponent内でeffectが発生させたい場合には,serverに向かって流れるstreamにメッセージを乗せ,その解決をserverに委託する.また,serverがeffectを解決した際には,clientに向かって流れるstreamにその結果を乗せて送信する.このHaskell <1.3のstream-based I/Oのような仕組みが,それぞれのeffectごとに個別に用意される.effectは互いに分離されており,異なる種類のeffectは異なるserverが解決する.effectは実際に副作用を引き起こして解決することもできる*3し,あるeffect Xを別のeffect Yに押し付ける形で解決することもできる*4.componentがもつすべてのeffectを取り除けば,そのcomponentを実行することができる.「なんだかよくわからないがある種の副作用の発生」が「それらの副作用は実際のところどのようにして引き起こされるのか?」に先立つため,componentから見ると実際のI/Oを意識する必要がない.

このようにして作られたcomponentは必然的にtestableなものになる.effectごとに関心が分離されているので,I/Oすべてをモックする必要はなく,そのcomponentが要求する最小限のdependencyさえ与えてやれば済む.更に,Cycle.jsのcomponentは,sources*5を受け取ってsinks*6を返す単なる関数であるから,どのようなdependencyを要求するかは引数の型情報として現れる*7し,serverに送られるrequestに対するテストを行いたければ,単に戻り値のstreamに対してassertionを書けばよい.また,引数としてstreamを渡せば済むのだから,テスト時には決め打ちのstreamを与えればよく,実際に副作用を引き起こす必要さえない.

また,componentが新たな副作用を要求するようになった際には,対応するserverを増やせばよく,拡張性も確保されている.

雑感#

「プログラミングが下手な人間は『ベチャッとした』コードを書く」という表現をよく使う.この『ベチャッとした』という表現はつい最近まで感覚的なものだったのだが,最近になってようやく言語化することができるようになってきた.それはどうも,準同型やら自然変換やらである程度潰れてしまった先の世界でコーディングをしている,くらいの意味のようである.

JavaScriptは非常に便利な言語で,なんと任意の場所で副作用を起こすことができる.Haskellは純粋な計算と純粋でない計算を分離したが,結局IOの中にはなんでも書けてしまう.副作用をダラダラ書くと,関心の分離が達成されない.純粋な計算でさえ関心の分離を達成したい場合もあるだろう*8.そう考えると,どの程度副作用を分離したいかというのはやはり程度問題で,ちゃんとやろうとするならば,domain-specificなsub言語を作っておいて,言語の記述と解釈のフェーズを分離し,より汎用(低レイヤ)の言語に解釈する,というやり方が必要になる.

最終的には結局,ライブラリが要求するmonad(例えばServantのHandlerとか)に落とし込む必要があるので,プロジェクトの要件に合わせたeDSL*9を書いて,そこからのHandler型への自然変換を与えて解釈する,というプログラムの書き方がいいのではないだろうか.そうすれば,解釈先を変えるだけで,ユースケースをCLIやバッチジョブからも実行できたりして便利そうである.

Cycle.jsの話ではなくなってしまった.まぁまぁ,どんな言語であっても,気持ちは似たようなものである.

というわけで#

株式会社HERPではCycle.jsやっていきエンジニアを募集しています!

先日会社から開発チームにdonationさせていただきました.

最後になりましたが,当日会場をご提供くださったサイボウズ株式会社様,誠にありがとうございました!

HERP広告

この記事はHERP勤務中に書かれた。

HERPは本物のCycle.jsプログラマーを募集しています。

脚注#

*1: これに対して「じゃあobservable/streamはその『複雑なメンタルモデル』とやらを要求しないのか?」という反論はもちろん可能だと思う.とはいえ,手続き的なプログラミングに慣れているか,それとも宣言的なプログラミングに慣れているか,という違いの問題だと思っていて,私にとっては後者の方が馴染み深い.

*2: 最近だと,CIでのビルドが通るとfaviconが変わるものも多い.

*3: driverを使って実際に副作用を起こすパターン.スライド中のDOMeffectおよびWebSocketeffectがこのパターンに相当する.

*4: スライド中のtoast effectがこのパターンに相当する.スライド中では,DOM effectに押し付ける形でtoast effectを解決している.

*5: server -> clientなstreamの束

*6: client -> serverなstreamの束

*7: もう2019年ですし,さすがにTypeScript書いてますよね?

*8: Readerなど.

*9: これはもちろんMonad型クラスのインスタンスにする.