ジンジャー研究室

長めのつぶやき。難しいことは書きません。

JavaScript フレームワークを巡った話

ポエムです。

自分の今の立場としては「Elm の人」ということになってるんだけど、どういう変遷でここまできて今どういうスタンスなのかっていうのはあんまり話す機会がない。だから整理のために考えてることを書いていくよ、というのがこの記事の趣旨。

非 Web の立場から

そもそも自分は「Web 系」の出身ではない。新卒入社したワークスでは ERP パッケージを提供するのに画面を Web 技術で作ってるというだけで、別に SEO の順位を競ったり広告をどうという話ではないし、瞬時に画面が表示されないと離脱率が〜という話でもない。ただ、画面はとにかく複雑で設定項目とががうじゃうじゃある。

あと、学生時代に PC に触れたのが Windows で「黒画面なにそれ美味しいの?」くらいに GUI に染まりきってたというのがある。工学系の研究を効率化するために C#GUI を作ってたら、なんかソフトウェア業界に入ることになってしまった。 CLI は今でも得意でなく、黒画面を前に「今なにをしようとしてたんだっけ」と手が止まってしまう。

話を戻すと、とにかく複雑で高機能な画面を簡単に作りたいわけである。仕事もそうだし、趣味の話で言えば音楽制作ソフトの華である DAW とかが作りたかったりする。

jQuery

最初は Java でスタンダードな画面遷移する Web アプリの作り方を学んでたりしたんだけど、なんかのタイミングで「 JavaScript を使えばグリグリ動く画面が作れるらしい」「とにかく jQuery を入れるんだ」という情報を仕入れて、まあやってみたら本当にグリグリ動いた。 あと社内でアリエルの製品を使ってて、かなり高機能な画面が JavaScript でゴリゴリ動いてるのを知っていたので、「そうかイケてる画面はこうやって作るのか〜」とか思いつつ、とりあえず目標としては JavaScript で高機能な画面を作ることとなった。

ところが自分が当時関わっていた社内システムは理想とは程遠く、よくある「とりあえずサーバー側で作った画面を jQuery でいじり倒す」という行き当たりばったり状態。まあそもそも JavaScript もほとんど分からない状態だったので、ポップアップを出すライブラリを適当に取ってきて用途に合わない動きをちょっとハックしてなんとか動かすみたいなことをしてた。

「このままじゃ全然イケてる画面になる気がしない・・・」と思っていた矢先、社内で「クライアントサイドの基盤でも作ろうか」みたいな話が持ち上がった。一応、技術基盤をやる部署だったので。

Ember.js

最初に検証の対象に持ち上がったのが Ember.js である。なぜこいつが最初だったのかはよく覚えていないが、確かにチュートリアル通りに手を動かすとイケてる感じのポップアップがポンと出てきて「お〜」みたいな感じになった。 これは本当にちょっとしかやってないからほとんど覚えてないけど、まあ JavaScript 自体が初心者ということもあり使い方が複雑で難しく、自力で組み立てようにもまずフレームワークが難しいような状態で断念。(あとマスコットキャラクターもあんまり好みじゃなかった。)

コンポーネント

フレームワークはよく分からなかったが、とりあえず目標としているのはネイティブ GUI のような高機能画面で、それらの特徴は「四角い部品」の寄せ集めで出来ているということだった。 当時「コンポーネント」という言葉を使ってたかは忘れたが、とにかく jQuery で HTML を直にいじるのはよくないから、もっと粒度の大きい部品を作って API 越しにアクセスすればいいんじゃないかという考えになった。で、そのコンポーネントを全体でツリー状にネストさせれば良いのだと。

Haxe

順番は忘れたが、 AltJS の波が来て「 Haxe っていうイケてる言語があるらしい」ということで、上記のコンポーネントはそれで作っていた。この頃には前よりいくらか JavaScript にも慣れていて Haxe 製の Haxe エディタなども作ったりした。

型もついて一件落着かと思ったが、「イベントをやり取りするのがめんどくさい」という課題が残った。例えば、「クリックしたら兄弟コンポーネントも更新する」ような場合に、一度親がイベントを拾って兄弟を描画し直さないといけない。あとは、遠く離れたコンポーネントを更新する場合には一度グローバルにイベントを渡すようなトリックも必要になった。 つまり、粒度は大きくなったが、やっていることとしては jQuery と同じ「ビューからイベントを受け取って別のビューを更新する」というフローになっていた。

これを打破するために必要になったのが MVC である。

Backbone.js

もはや名前も聞かないが、当時 MVC フレームワークと言えばとりあえずメジャーな Backbone.js かなという雰囲気があった。jQuery を使うのが前提になっているのも、その時代という感じがする。 こいつは何かというと「モデルが更新されたらビューが自動で更新されるようにしましょう」が軸で、手段としてはオブザーバーを使う。ビューはモデルを listen し、モデルのプロパティが更新されたという通知を受けたらなんらかの方法で再描画する。

仕事では試す機会がなかったので、趣味で Backbone.js で色々作ってみることにした。確か WebAudio API の UI を作ったのはこれ。で、色々問題が見つかった。

まず、イベントハンドリングがスパゲッティになる。モデルやそれの集合体であるコレクションを更新するととにかくイベントが飛びまくる。「1回の更新のはずなのになぜか3回再描画が走る」などのトラブルが起こり、原因を突き止めるのがとにかく大変。 次に、リスナーの削除が漏れる。ビューのライフサイクルの中できちんと unlisten できればいいのだが、上位コンポーネントが雑に innerHTML とかでまるっと消してしまうと、リスナーが削除されずに溜まっていく。 最後に、モデルを更新したら自動的にサーバー側と同期をとる仕組みが意味不明だった。そんなに上手くいくんだろうか? Rails の何かを真似した仕組みっぽいけど、 Rails の経験はなかったし(今もない)、そもそもサーバーと同じ仕組みで動くものだろうか。

あとは Backbone.js に何かの仕組みを付け足した派生みたいの(Marionet とか)が結構あったけど、どれも大成功しているようには見えず、モヤモヤしていた。

AngularJS

当時はとにかく AngularJS の登場が衝撃的で「すげー!」となった。(登場というか、当時はそんなにネットで情報収集していなかったので、すでに登場していたのかもしれない) もちろん Angular 1 のことである。

仕組みは Backbone.js よりもかなり単純で、モデルを更新したら次のターンでビューを描画する。テンプレートを使い、今の Virtual DOM に似た dirty checking という方法で変更差分だけを DOM に反映する。ただ、非同期処理が入ると描画がトリガーされないので、そこだけは上手く専用の API を呼ぶ必要がある。

つかみは非常に良かったのだが、それ以上複雑なことをしようとすると、どうすればいいのかたちまち分からなくなった。コンポーネントをツリー状にすれば良いんだろうと思っていたが、そんな説明はドキュメントのどこにもない。directive というカスタム要素を作る方法はあったが、ちょっとした用途向きで無限にネストすることは考えられていない。他にもコントローラーとかインジェクションとかも色々あったが、とにかくどう使えば良いのか分からない道具が並んでいるという印象。

当時「AngularJS を使っていると何度か精神的にアップダウンするけど、最終的には最高の状態に行きつける」みたいな言説があったけど、自分はアップダウンしている間に燃え尽きてしまった。

Polymer

Google が「時代は Web Components !」と言わんばかりに Polymer を突っ込んできた。 Web Components は自分の当初やりたかったコンポーネント構想に近かったのもあるし、 Web 標準にもなるというのでかなり期待が高かった。Chrome 35 だか 36 の時に Custom Elements が実装されて大はしゃぎした記憶がある。

もう記憶がおぼろげだが、 Polymer は単なるポリフィルというよりもフレームワーク的なこともお世話してくれていた気がする。今は虫の息と言われている HTML Imports とかも、本番用に結合して配信するための vulcanize とかいうツールが作られたりした。作ったコンポーネントをシェアするためのリジストリなんかもあった。 Polymer のカンファレンスもあった。とにかく「これからみんな Polymer でやってくぞ」という雰囲気で宣伝も激しかった。

で、自分のホームページを Polymer で構築してみた。 ・・・当時は「未来の Web 標準」なんて Google が言い出したものなら「なるほどそうなのか!」と言って無邪気に信じたし、ライブラリだって「凄い人たちが作った凄いもので自分は使うだけ」と思ってたから、まさか数ヶ月後に突然ホームページが真っ白になるとは思わなかった。多分 Chrome の何かのアップデートで polyfill の一部が壊れたのだろう。 すっかり懲りてしまって、「まあ Google がなんか言っても話半分に聞いとくか」くらいのスタンスになった。

リアクティブプログラミング

そんなこんなしている時に、ひょんなことから次の記事が目に留まった。

なぜリアクティブプログラミングは重要か。 - Conceptual Contexture

概念としてすごく面白そうだと思って「これ MVC フレームワークに応用できるんじゃね?」となった。 アイデアとしては、モデルが変更されたら自動でビューが更新されるように連鎖してくれれば言いわけだ。AngularJS とかだとモデルとビューは1対1なんだけど、リアクティブプログラミングであれば「モデル => モデル => モデル => ビュー」のようにスケールしそうな気がする。

というわけで、それができそうな Bacon.js を触ってみる。だけど Bacon.js はイベントの起点が DOM になっていて、なんかちょっと違うなと思った。(少なくとも当時はそう思っていたけど、今は別におかしくないと思う) で、もう少し調べていると、なにやら Elm という言語が FRP(関数型リアクティブプログラミング)というのでそれっぽいことをしているらしい。こんにちは、FRP

ただ、当時の Elm は難しかった。Haskell を知っているのが前提みたいな空気があったし、何と言っても HTML タグが書けなかった。頑張って習得したところで canvas に5角形を書くくらいしかできない。「TodoMVC をなんとか Elm でも書けたぞ!」と Evan が得意げに YouTube に投稿するくらいである。 当然「流石にこれはオモチャでしょ...」となった。

React

これはもう魂が震えてしまったね。もはや説明の余地もない「宣言的ビュー万歳」である。 最初は「そんなバカスカ Virtual DOM 生成していいものか」という不安はあったが、使ってみてすぐにその懸念は払拭された。コネで CodeZine に2本ほど記事も書いた。

もちろん全てに満足していたわけではなくて、「わざわざシンタックスのためだけに JSX とか入れる?」とか、「 JS で Immutable 守り通すのきつくね?」とか、まあ色々思うところはあったが、出発点が jQuery のカオスだっただけに「まあ言っても昔に比べりゃ全然 OK なレベル」と概ね満足していた。

Elm

そんなところに「Elm が Virtual DOM を導入したらしい」というニュースが入ってきた。(これもニュースというよりは、少し経ってから知ったのだが)

「Elm で HTML が書けるってマジ?」

元々関数型も少しかじってたし面白そうだからやってみるか。で、試しに色々作ってたら「なんかこれ普通に実戦投入できそうじゃね?」になって今に至る。その間に Elm は FRP であることをやめてしまった。さようなら、 FRP! あと、ミートアップに顔を出したり主催してたら Idein 社に呼んでもらえた。ごめんな ERPFRP もやめてしまって。

仕事でも Elm を快適に使えているし、大体の用事は Elm でなんとかなる。 そのことに特に不満はない。シンプルながら最強の型システムを持ち、コンパイルは爆速、ライブラリはフロントエンドに特化しているし、関数単位でデッドコードを除去しながらバンドルまでしてくれて、いざとなれば JavaScript と連携可能である。これだけあって、至らない部分に文句を言うのは贅沢というものである。

ただ、個人的な問題としては、Elm Way が完成されすぎてて「もっとこうしたらどうか」と自由に遊べない。「遊んで時間を浪費するよりも余計なことを考えずにトレードオフを受け入れろ」という、なんとも仕事向けな奴なのである。(言語機能は汎用的だが、主要ライブラリの仕組みごと作り変えるコストが高い) 「宣言的ビューは善である」という仮説をある程度検証することは出来たけど、「じゃあ他の可能性はなかったの?」と思わなくはないわけである。

Web Components

Edge が開発を断念し、世はまさに Chromium 一強時代に突入しようとしているらしい。 となると、 Web Components (主に Custom Elements と Shadow DOM )が意外と早く陽の目を見るのでは、という見方からパラメーター配分を変えているのが現在の状況である。なんとなく v1 はこのまま行きそうだし、 Polymer に頼ることもなさそうだ。

Web Components はフレームワークではないので、素のまま使うのは難易度が高いかもしれないが、今はあえて「何も使わずにやる」という方針にしている。というのは、今のフレームワークって全部 Web Components がない前提で作られたものばかりなので、 Web Components があるとまた違った景色が見えてくるかもしれないから。「素のまま頑張るとこんなもんか〜」と生 DOM の感触を確かめつつ(一応 TypeScript で)色々と試している。

今のところの感想としては、素の Web Components は Backbone.js と書き味が近い。 ただ(自分の理解では) unconnectedCallback が必ず呼ばれるはずなのでリスナーの解除し忘れがないのが嬉しい。独自の仕組みとしてイベントに EventTarget を使っているが、モデルとは紐付けないようにしている。HTML をテンプレートで書くときも、VSCode の lit-html プラグインを使うとシンタックスハイライトが効いたりする。

その先

どうなるかは全然読めないけど、とりあえず Web Components をベースにガチャガチャやっていくことになるんだと思う。CSS は Shadow DOM の中で干渉なく使えて css-loader とかの存在意義が薄れる。モジュールは基本 ES6 Modules で書かれるけど HTTP/2 でサーバープッシュするプランが怪しいので、引き続きバンドラーを使うことにはなりそう。言語はそのうち TypeScript をブラウザが解釈するようになるんじゃないか、などと妄想しておく。

自分の探求する領域としては、 Virtual DOM alternative を考えようかなと思っているところ。 lit-html や hyperHTML みたいなものかもしれないし、別のかもしれない。Elm は仕事で継続してベストプラクティスを探求しつつ、 Web Components との組み合わせも試したい。そんな感じ。

おまけ:その他のライブラリ

  • jQuery UI: 単品でいくつか使ったことがある。
  • dojo: 聞いたことある程度。
  • Knockout.js: 聞いたことある程度。
  • ExtJS: 前職で使われていて良さそうだったが、あまり触ってない。
  • Google Closure Library: これも前職やアリエルで使われていたが、玄人向けで使いこなせる気がしなかった。
  • RxJS: 気になりつつ触ってない。
  • Angular(2-): ちょっと何がしたいのか良く分からない。
  • Redux: だいたい知ってるけど触ってない。昔の Elm の名残りで副作用周りがミドルウェアに追い出されてるように見える。
  • Vue: チュートリアルはやった。ルールが多いのとオブザーバーへのトラウマから食わず嫌い中。
  • Riot.js: 正規表現でゴリゴリパースしてるという話だけ聞いた。
  • Dart: 初代をちょっと触った。
  • Flutter: よく知らない。
  • Preact: よく知らない。
  • Mithril: 一時期速いって言われてたことだけ知ってる。
  • choo: Elm 再現系ね。
  • Cycle.js: あの人最近何やってるの。
  • Meteor: 密結合が怖くて触れなかった。
  • CoffeeScript: 触る前に消えた。2は知らない。
  • Reason (React): 型はあるけど Elm みたいに API ドキュメントが見当たらなくて辛かった。
  • PureScript (Halogen): 自分には難しそう。
  • ClojureScript (om): よく知らない。
  • Scala.js: 触ってない。
  • js_of_ocaml: あったね。

deno で Elm の live reload を作ってみた + 感想

deno はこれ。

github.com

Node.js 作った人が今度は TypeScript で作り直してるっていう話らしい。

yosuke-furukawa.hatenablog.com

で、今はまだまだ実用段階ではないんだけど、一応それなりには動く模様。

まじで、置いとけば動くの。

f:id:jinjor:20181223092317p:plain

うおー本当だ。なにこれ楽しい!

試しに作ってみた

github.com

deno で Elm のライブリロード。 こんな感じで起動して、ソースの .elm ファイルを更新するとコンパイルしたついでに画面が更新される。

deno ../index.ts src/Main.elm src/index.html  --port=3001 --allow-net --allow-run

以下が一通り試せるので、お題の選定として最適っぽい。

  • 引数処理
  • ファイル読み込み
  • プロセスの実行
  • HTTP サーバー

感想

ついさっき知ったことをそのまま横流しに紹介するスタイル。 ちゃんとした紹介はあとで誰かにしてほしい。

deno https://...

一番面白い点としては、やはりモジュールがインターネットから降ってくるところ。1回目でフェッチして2回目以降はキャッシュを使うのでオフラインでも大丈夫。--reload をつけると読み直す。~/.deno につないだ場所とか履歴とか生成した JS とか source map とか諸々置いてある。

「いやいやそんな適当なモジュール管理でいいわけないでしょ」っていう反応に対していくつか考えが述べられている(けど、もう少し詳細に知りたい気持ちはある)。 https://github.com/denoland/deno/blob/master/Docs.md#linking-to-third-party-code

バージョンは URL で一意に決まるから npm とか package.json みたいなやつはいらなくて、その辺は例えば package.ts を用意するとかしてアプリケーションコードで管理してくださいとのこと。随分と思い切るなー。

特定の操作に許可が必要

フラグをつけないとファイルに書き込んだりネットにアクセスしたりできない。V8 のサンドボックス機能を活かした形。

        --allow-write   Allow file system write access.
        --allow-net     Allow network access.
        --allow-env     Allow environment access.
        --allow-run     Allow running subprocesses.

サンドボックスの外とはシリアライズされたデータでのみやりとりが出来る。

deno/msg.fbs at master · denoland/deno · GitHub

とにかく Promise (async/await) を使う

標準から Promise API で溢れているので、もうとにかく async/await し放題。将来的にはトップレベルでも await したいとのこと。あと Unhandled なんとかはもれなくエラー。

型付き API ドキュメント

嬉しい!

https://deno.land/typedoc/

まだ洗練されていない印象はあるものの、ちゃんと型を辿っていけば正解にたどり着けるようになっていた。すごい。

まだ楽に色々出来る状況ではない

今提供されている API はかなり最低限のもので、ファイルを読むにもオープンしてバッファにデータ詰めてデコードして最後にクローズ、というのをやる必要があり結構だるい。TextDecoder / TextEncoder とか知らないと辛いんだけど、調べてもすぐに出てこないので「目の前に string があるけど API が要求しているのは UInt8Array でどうしよう」みたいな事に、よくなる。

HTTP サーバーはなんとか動くレベル

一応、標準ライブラリがある。

github.com

が、 HTTP サーバーは基本機能もまだ怪しくて、headersContent-Length を正しく入れてやらないとコネクションが閉じず、ブラウザから1秒おきに fetch() すると7リクエスト目から通信不可能になってしまった。あと最近まで POST の body が読まれないことがあったらしいとか、そういう状況。

引数処理が楽

同じく標準ライブラリの flags なんだけど、これだけのために deno を使いたいくらい良く出来てる。 これが npm の argv 相当で、他にも npm の chalk 相当の color とか、「それが欲しかったんだよ!」っていうのが用意されててとにかく嬉しい。

他に面白そうな要素

今回は通らなかったけど、 deno のネイディブ側は Rust で動いているらしく、deno を Rust 内で使うにはこのクレートを使えばよいそうだ。楽しそう。

貢献のチャンス

今なら既存の何かを deno 用に書き直すだけでワンチャンありそうだし、 deno 独自の特徴を活かした面白いものが色々作れるかも。 腕に自信のある方は是非。

GISC : Scaffolding のネタを Git のリポジトリから取ってくるツール作った

f:id:jinjor:20181107032402p:plain:w200

github.com

GitHub の適当なリポジトリを Scaffolding の元ネタに使っちゃおうぜ というツールを作った。

Git で Scaffolding なので GISC 。大文字でも小文字でも可。gisc という名前にしたら disc っぽかったのでそういうロゴにした。ちなみにロゴは会社帰りに iPhone のメモアプリで描いた(便利!)。

使い方

インストール。

npm install -g gisc

jinjor/gisc リポジトリexample ディレクトリをコピって my-project を作る。デフォルトだとサーバーが github.com 、プロトコルhttps 、ブランチは master 。 depth は常に 1 。キャッシュはしない。

gisc get jinjor/gisc example my-project

これでもまだ長いので alias を作って同じことをする。

gisc add ex gisc example
gisc ex my-project

サブコマンドは get ls add remove share がある。alias は ~/.gisc に保存される。

モチベーション

Elm で main 関数を書くハードルが高い。 というのは、 Elm はフレームワークを組みこんだような言語なので、 init, model, view のような決まった関数をいくつか用意しないとスタートラインに立てない。書き方がいくつかあって、 SPA 機能を組み込んだ最終形態が これ 。main だから1アプリに一つなんだけど、色々と試したり遊んだりサンプル作ったりするので、なんやかんや作る機会が多い。その度にどこかからコピってきてた。

実は elm init というコマンドがあるんだけど、このコマンドは elm.json しか作ってくれない。というか、本当は Main.elm を作る機能があったんだけど 0.19 リリース直前に削除された。まあ、こういうふわふわしたものを作って「自分の好みとちょっと違う」とか文句を言われたり自分で管理するのが嫌なんだろう。

そういえば昔そんなツールあったよねと思い出したのが elm-new なんだけど、残念ながらメンテされてない。まあ OSS なんてそんなもん 。フォークしてもいいけどあんまり好みの構成になってないというか、純 Node だったらなあみたいな気持ち。

で、そういうことをするのは Scaffloding と言って昔からツールがあるよね、ということで一昔前に話題になった Yeoman とかも見たんだけど、正直使ってるという話を聞いたことがないし、前にちょっと触った時はとにかく使い方が難しくて「どうしてテンプレートをコピーするだけのためにこんなに大掛かりな物が必要なんだ」と思った記憶がある。

もう少し調べると giter8 というのがあって、でもこれは Scala プログラマ向けっぽい匂いがする。 Node がいいよなーと思ったんだけど、いまいち「これ!」という物が見つからない。

よくよく考えると GitHub から取ってきてコピーすればいいだけなんだから、GitHub の raw ファイルを curl で取ってくればいいんじゃないかと思ったんだけど、まあ URL 長くて覚えられないので管理を簡単にしたい。あと、ディレクトリがコピー出来ない。他にも rsync とか scp とか、それの Git 版(?) みたいなやつを検討したけど、どれも今回の目的にはマッチしないなという感じ。

この辺まで調べてようやく「どうやら既存のいいものは無さそう」と思い始め、同時に 車輪を作る理由を得た

こだわった部分

とにかくシンプルかつ簡単にしたかった。

名前は最初 gitscaf って名前だったんだけど、7文字も打つのはかったるいし 自分がコマンド名を覚えられなかった ので短くした。4文字なら使う気になる。

テンプレート機能はつけない。そんなことをしたら難しくなる。コピペして書き直せばいい。

最初は get しか出来なかったけど、長くなると覚えられないので addエイリアスを登録できるようにした。せっかく alias を登録しても覚えられなければ意味がないので ls は必須。--help も。とにかく記憶力がないと使えないので CLI は嫌いだ。

あとは独自ルールも排除。最初「 init という実行ファイルが置いてあったらそれを実行する」みたいのも考えたけど、それを説明するのも面倒臭い。それに色んな人をエコシステムに巻き込むコストが高い。第一そんな物があると pre だの post だの色々欲しくなってしまう。セキュリティもある。とにかく面倒なのでやめた。

ローカルのファイル・ディレクトリは登録できない。それをするとマシンを跨いで作業した時にデータがなくなって後悔するので、最初からリモートのみにする。

contrib みたいな中央集権的なこともしない。何もメンテしたくないし、ブロックもしたくない。思うのと違ったら勝手にフォークするなり別のリポジトリから取ってもらいたい。

既に十分便利なんだけど、良い設定が出来たら他の人にもシェアできるように share コマンドを作った。gisc share コマンドを実行するとエイリアスを登録するためのコマンドが出る。

$ gisc share
gisc add elm.button evancz/elm-architecture-tutorial examples/01-button.elm
gisc add elm.field evancz/elm-architecture-tutorial examples/02-field.elm
gisc add elm.form evancz/elm-architecture-tutorial examples/03-form.elm
gisc add elm.random evancz/elm-architecture-tutorial examples/04-random.elm
gisc add elm.time evancz/elm-architecture-tutorial examples/05-time.elm
gisc add elm.http evancz/elm-architecture-tutorial examples/06-http.elm
gisc add elm.sandbox jinjor/gisc-elm src/sandbox.elm
gisc add elm.element jinjor/gisc-elm src/element.elm
gisc add elm.document jinjor/gisc-elm src/document.elm
gisc add elm.application jinjor/gisc-elm src/application.elm
gisc add elm.package jinjor/gisc-elm elm.package.json
gisc add elm.index jinjor/gisc-elm index.html

コピペするだけでエイリアスをサクッと追加できる。他にも色々アイデアはあるので、とりあえず ここ にまとめた。(飽きっぽいので続く保証はない)

作ったテンプレート

というわけで、 Elm 用のテンプレ作った。

嬉しい!やったね!

Elm で不正な JSON に厳しすぎる問題についてのメモ

長いだけで中身は単なる雑なメモなので、結論や主張はないです。

起こりうる問題

Elm で、サーバーがおかしな JSON を返してきた時にエラーが発生してアプリが止まることがある。一般的にはシステム境界でエラーが分かるのは嬉しいのだが、不寛容すぎると問題を起こしうる。 これは JavaScript とかだと起こらない問題で、 Elm とかだと起こりうる。例えば、{ name: string } みたいな API だとして {} が来た時に 「nameがない!」とエラーになる可能性がある。これを デコードエラー と呼ぶ。それが本番で運用した時に問題にならないか、という話。確率は低そうだが考えておいて損はなさそう。

Elm 以外も含めて言語別に見ると、

  • JS の場合、プロパティが null や undefined でも構わず処理を続け、運悪く当たるとぬるぽになる。型が違った場合は適当に処理される危険性がある。そのプロパティが運よく使われなかった場合は無事に処理が回る。
  • TypeScript の場合も、JS と同じ。型が違う場合はコードに書いてある型が嘘をつくので混乱するかもしれないが。
  • Elm の場合、JSON をデコードした時に間違っていればエラーになる。無いはずのプロパティがあっても問題にはならないが、その逆や型が違う場合にはエラーになる。

もちろんこれは API 側の問題なので、開発中は早期に問題が発見できて助かる。実際に、React から Elm に乗り換えた NoRedInk では、サーバー側(Ruby)のバグがクライアント(Web UI)側で見つかって便利みたいなことを報告している。

Fun Fact: I have heard a bunch of stories of folks finding bugs in their server code as they switched from JS to Elm. The decoders people write end up working as a validation phase, catching weird stuff in JSON values. So when NoRedInk switched from React to Elm, it revealed a couple bugs in their Ruby code!

Fun とか言ってるので、多分 NoRedInk では問題になっていない。実際 NoRedInk 的には何十万行の Elm コードがプロダクションで動いていて今までにランタイムエラーが1件とのことなので、少なくともランタイムエラーにはなっていない。ちなみに Elm では Debug.todo(旧 Debug.crash)だとランタイムエラーが発生して、画面は表示されてはいるがアプリケーションの動作としては完全にストップする。一方、それ以外の場合は、どこかしらでエラー処理されているか無視されているということになる。

想定ケース

具体的にどういう問題が起こるのか、それとも大して問題にならないのか。

大まかな前提としては、(A)初期化時に GET して (B)各ページの表示のためにまた GET して (C)それぞれのページで操作中に POST その他が発生する、というよくありそうな SPA とする。Debug.todo しない場合エラーを画面に表示する処理は書いてあるものとする。

どこで 何が起きると どうなる
A-1 初期化 Debug.todo 真っ白かガワが見えた状態でストップ
A-2 初期化 デコードエラー 真っ白かガワが見えた状態でエラー表示、その後の操作は受け付けるがメイン画面が出ないのでメインの処理はほとんど何もできない
B-1 ページ表示 Debug.todo ページ遷移前または遷移後にガワが見えた状態でストップ
B-2 ページ表示 デコードエラー ページ遷移前または遷移後にガワが見えた状態でエラー表示、その後で他のページには行ける
C-1 ページ操作 Debug.todo 処理後にストップ
C-2 ページ操作 デコードエラー 処理後にエラー表示、その後で別の操作はできる

また、上のそれぞれについて 将来的に他の依存しているサービスの API が勝手に変わる ことも考えうる。普通に運用していたら、いつの間にか死んでいたというパターン。というか、意外と世の中的にはそのパターンが多いのかもしれない。考慮に入れておこう。

いずれにしても、動作を完全にストップ Debug.todo は罪深い。しかし Debug.todo は書かなければ良いので、回避するのは容易そう だ。というわけで、とりあえずそれ以外にフォーカスする。

A-2, B-2, C-2 共になんらかの処理が不可能になってしまうのが問題のように思える。しかし、間違った処理をサーバー側に伝播させるよりはマシとも言える。10 + 010 だが、 "10" + 0"100" になり、いつの間にか 10 万円のつもりが 100 万円払っていたような事故は防げる。反論としては「どこかでランタイムエラーになるんじゃね?」or「極端な値だとバリデーションで弾かれる」or「テストしてるから大丈夫」はありそうだが、確率の話になってきそう。あるいは JS でなく "10" + 0 をランタイムエラーにしてくれる言語があったとして、それで良いのかもしれないが。

とりあえず、ここでは「全くチェックしなくても確率的に大丈夫そう」は話が発散するので除外しておこう。テストはした方が良い、それは間違いない。しかし我々は型の恩恵をフルに受ける言語を選んだのだから、別のところでメリットは出ているはず。その前提で、ここからは問題解決の方法について検討したい。

チェックの粒度とタイミング

「どこかがおかしくても一部の処理は生き残っていて欲しい」が要望だとすると、言い換えると問題は次の2つになる。

  • 一度にチェックする粒度が大きすぎる
  • チェックするタイミングが早すぎる

あるいは「なるべく小さい粒度で、ギリギリまでチェックを遅らせて欲しい」ということになるのかもしれない。

何か例が欲しい。次の User 型をデコードしよう。

decodeUser : Decoder User
decodeUser =
  map3 User
    (field "id" int)
    (field "name" string)
    (field "age" int)

今、 { id: 1, age: 20 } だとすると "name" が足りないのでエラーになる。が、"id" を使った処理はできるはず。 ここまで書いて気づいたけど、「未然に防ぐには?」なのか「起きてしまった時にどう対処する?」という2種類の話がある気がする。未然に User の型を捻じ曲げるのは不可能と思われる。例えば id name age 全てを Maybe にするとか、考えたくもない。しかし事後で良いなら name だけを Maybe にするのは可能そうだ。それで改めて何かの処理をしようと思ったら「残念、値がありませんでした!」と言える。それは十分現実的なので、なるべく早めに察知できる仕組みを整備したい(それがデコードエラーであることがすぐに分かるのはメリットだ)。

あとは、デコードを遅らせる手として Json.Decode.value を使うことも検討したい。要するに GET した時点では任意の JSON として取得して、使う段になって改めてデコードする。これならば、例えば id が必要となった時に id だけをデコードして使うことができる。

decodeUserId : Decoder Int
decodeUserId =
  field "id" int

とは言え、全てのデータに関してこれをやるのは正直だるすぎて死ぬ(ひょっとして Haskell だったら遅延評価だから素でこういうことができる?)ので、使いどころを絞りたい。例えば、独立した機能のデータを一度に取得した場合に

type alias Features =
  { feature1 : Value
  , feature2 : Value
  , feature3 : Value
  }

とでもしておいて、それぞれの機能を使う段になって初めてデコードする。これは業務でも似たようなことをやったことある、けどデコードエラーが十分想定しうる場合だったような気はする。全く想定外だったらこういう構造になるか怪しい。

今のところ考えられる対策

まとまらないけど、とりあえずこの辺で。

  • 少なくとも Debug.todo は避けよう
  • エラーハンドリングしてサーバーに通知する
  • 事後で良ければ Maybe などを使ってエラーを遅延できる
  • 場合によっては Json.Decode.value を使ってデコードのタイミングを遅らせることはできそう

時間帯によって特定サイトの閲覧を禁止する for macOS

f:id:jinjor:20181024032631p:plain

TL; DR

cron で /etc/hosts を書き換える。

* 6-22   * * * /usr/bin/sed -i '.bak' -E 's/(.*)#block$/#&/g' /etc/hosts
* 0-5,23 * * * /usr/bin/sed -i '.bak' -E 's/^#+(.*)#block$/\1#block/g' /etc/hosts

以下、この方法に到るまでの経緯。

動機

SNS が時間泥棒だからアクセス出来ないようにしたい、というのは昔から言われているのだが、特にまずいのは Slack だと思っている。Twitter は流しておけばいいんだけど Slack はチャットなので会話が発生する。見るだけなら問題ないじゃんと思いつつ、見るとつい返信したくなる。そういう人が2人以上いると無限ループになり深夜に突入。で、他の人が朝に見たらなんか会話が進んでいる状態。よくない。

なんとか Slack を使うのを止める方法はないのか?

対策を考える

で、色々と検討したのをメモ。前提条件としては macOS

bot に怒ってもらう

Slack だから、深夜時間帯に使ったら個人宛に怒ってくれる bot とかがいれば、書き込みを抑止できるのではないか。

結論としては、睡眠モードに通知を出さない機能はあるが bot は無さそう 。 作ることは出来そうだが面倒。(調べてないけど個人単位で bot の導入できるんだっけ?全員にルールを適用されるのは嫌だ。それやったら朝に強制ラジオ体操やるのと変わらない。)

ペアレンタルコントロール

子供が深夜などにアプリを使用するを制限する機能のようだ。なるほど、大人の皮を被った子供は潔く子供扱いされねばなるまい。 ただし、管理者は子供になれないので子供用のアカウントを作らないといけない。大人のアカウントで設定して子供の方にログイン…うーん面倒。

SelfControl

SelfControl という自制心の効かない大人のためのアプリがある。ブラックリストに入れたサイトに一定時間アクセス出来なくなるらしいのだが、そのボタンは自分で押さなければならないようだ。いや、そんなに意志は強くない。

Airplane Mode

Airplane Mode という同じく自制心の効かない大人のためのアプリがある。てか、 Evan さん何やってんすか。デスクトップにアイコンを置いて、叩くと /etc/hosts を書き換える Python スクリプトが走る。ただこれも手動で叩かないといけないのがな〜という気分。

cron で /etc/hosts を書き換える

ズバリというものがなかったので、/etc/hosts を書き換えるというアイデアだけ拝借する。 ブロックしたいサイトに #block のような印をつけておいて、指定した時間になったらその行を sed でコメントイン・アウトすればいい。

0 06 * * * /usr/bin/sed -i '.bak' -E 's/(.*)#block$/#&/g' /etc/hosts
0 23 * * * /usr/bin/sed -i '.bak' -E 's/^#+(.*)#block$/\1#block/g' /etc/hosts

正規表現には自信がないが、以下がコメントアウトされるはず。

...

0.0.0.0 xxx.slack.com #block
::      xxx.slack.com #block

これで OK 、のはずだった。

DNS の反映が遅い

しかし DNS の反映が遅い。

ということでググると、 DNS キャッシュをクリアする方法が OS 毎に出てくる。で、色々試して最後に残ったのが ChromeDNS キャッシュで、chrome://net-internals/ にアクセスしてボタンを押すしか方法がなく、コマンドで消せない。 「じゃあ puppeteer でボタンを押そうか」という事で調べたら、すでに同じことを試みた人が出来ないと言っていて、さらにたどると chromium のフォーラムで「headless モードで chrome://net-internals/ にはアクセス出来ないよ」という回答が付いていたので、意図的っぽい。

というわけで DNS の反映は諦めた。まあ 30 分くらい早めにすれば多少のずれは良しとしよう。

スリープ中に cron が走らない

23時にセットしたから今頃見えなくなっているはずで、と思って Slack にアクセスしたら超見え見えじゃん〜。cron の設定は正しいっぽく見えるので、やはり mac がスリープ状態だと無理なのか。

調べると mac を不眠に陥れようという酷い方法が提案されていて、心が痛んだのでやめておいた。 さらに調べると、sleepwatcher というツールを使うと起動した時にスクリプトを流すことができるらしい。が、情報少ないし信頼性もわからなかったのでやめておいた。

毎分実行する

そして、最終的に「起きてる間に毎分走らせりゃいいか」ということになった。

* 6-22   * * * /usr/bin/sed -i '.bak' -E 's/(.*)#block$/#&/g' /etc/hosts
* 0-5,23 * * * /usr/bin/sed -i '.bak' -E 's/^#+(.*)#block$/\1#block/g' /etc/hosts

ちゃんと動いていそう!

まとめ

もっと良い方法があれば教えてください。

依存の多い npm のパッケージをあぶり出す

直接依存しているパッケージが間接的に依存しているパッケージ数を知りたい。 npm ls でそういうオプションがありそうだけどないような?

仕方がないのでスクリプト書いた。

const cp = require("child_process");
cp.exec("npm ls", (e, out, err) => {
  const results = [];
  out.split("\n").forEach(line => {
    if (line.charAt(0) === "├" || line.charAt(0) === "└") {
      const splitted = line.split("@");
      splitted.pop();
      const name = splitted
        .join("@")
        .split(" ")
        .pop();
      const p = [0, name];
      results.unshift(p);
    } else if (results.length) {
      results[0][0]++;
    }
  });
  results
    .sort((a, b) => b[0] - a[0])
    .forEach(([count, name]) => console.log(`${count}\t${name}`));
});
15   json-schema-deref-sync
12  better-ajv-errors
11  ts-node
6   chalk
5   ajv
4   axios
2   xregexp
1   @types/chalk
0   typescript
0   openapi3-ts
0   @types/xregexp
0   @types/node

本当はここから気になった箇所を GUI で掘っていけると便利。

画像の差分を見つけるツールを作った

f:id:jinjor:20180907111251p:plain

作ったのは大分前なんだけど、想定するユースケースで実際に使えそうだと確認できたので。

作ったもの

github.com

動機

  • デザイナーから新しいカンプをもらった時にどこが変わったのか分かりにくかった
  • 作った機能をレビューしらもらう時にスクショのどこが変わったのか分かりにくかった

微妙なところ

完成度はぶっちゃけ高くないというか、自分の用途のために使う MVP 的なやつなので最低限しかできない。 具体的には、

  • 遅い
  • PNG のみ
  • 上下にずれると全部変わったことになる

遅いのはアルゴリズムが愚直なのもあるけど、せめてファイル変わってない時は checksum 取るくらいの対策はしたい。 最後のは改善したかったけど蓋を開けたらそういう修正はほとんどなかったというか、あってもあまり問題にならなかったので放置。

ところで画像差分検知と言えばもっと有名なのがあるので、ちゃんとしたやつを使いたい人はこっちを使ってね!

github.com

こっちも試したけどリグレッション検知が目的っぽいので、ちょっと UI が合わないという些細な点が気になってしまった。