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

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

connpass.com

発表資料はこちら.

qiita.com

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

twitter.com

発表内容のサマリ

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

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

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

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

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

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

github.com

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

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

Web フロントエンド・アプリケーションにはとにかく大量の副作用が付いて回る.仮想 DOM を元にした画面の(再)描画や,ユーザが起こした clickinput などのイベントの取得,HTTP を通じた API へのリクエストの送信・レスポンスの受信,アプリケーション自体のステート管理,WebSocket を通じた通信,History API を用いた履歴管理,document.title の更新,favicon の変更*2,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 やっていきエンジニアを募集しています!

www.wantedly.com

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

opencollective.com

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

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

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

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

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

*5:server -> client な stream の束

*6:client -> server な stream の束

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

*8:Reader など.

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