ジンジャー研究室

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

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 が合わないという些細な点が気になってしまった。

2つの順序キーの間のキーをいい感じに生成するライブラリを作った

RDB で ORDER BY するためのカラムを持つ時に、並び替えや挿入がうまく出来なくて困った。

f:id:jinjor:20180905182715p:plain

例えば、このテーブルで B と C の間に E を差し込みたい時に、

f:id:jinjor:20180905181617p:plain

こうなってくれると嬉しい。

作ったもの

🎉

github.com

TypeScript 用に書き直してくれてもいいのよ?

仕組み

  • キーは 0-9A-Za-z の 62 種類の文字が使える、ただし、最後の文字が 0 であってはいけない
  • 最初のキーは 1
  • 次のキーは「既存のキーの次」か「既存のキーの前」か「既存の2つのキーの間」のいずれかを指定して生成する
  • キーの左の桁を優先的にインクリメントしようとするが、無理な場合は桁を増やしてインクリメントする
  • 例:
    • between "1" "3" == "2"
    • between "1" "2" == "11"
    • between "1" "11" == "101"
    • after "1" == "2"
    • after "z" == "z1"
    • その他

技術的な話

Fuzzer を使って 10 万テストケースを自動生成して回しています。

今回は 0, 1, z などの境界値付近を重点的に攻めるために確率を操作しています

参考リンク:

Elm 0.19 の主な変更点

祝 Elm 0.19 リリース!

https://elm-lang.org/blog/small-assets-without-the-headache

1年半ウォッチしていたので覚えている範囲で書いてみる。

追記

↓ここに全部書いてあるじゃん。というか上の記事からリンクされてたし、この記事いらないじゃん。

github.com

コンパイルが速くなった

タプルで大量にパターンマッチした時に遅くなる件も改善。

--optimize でサイズの最適化

出力される JS のサイズが小さくなる。 関数単位のデッドコード除去が可能(Google Closure Library の advanced compile 相当) Debug モジュールを使用していると --optimize できないので注意。

単一コンストラクタでメモリを消費しなくなった

これは --optimize をつけた時だったかな、覚えてない。 type T = T Foo みたいにした時に T で包むのを省略する。

シングルバイナリになった

Elm Platform はもうない。 elm-make の代わりに elm make と打つ。

コマンドラインの刷新

  • --warn が消えた
  • elm init で最低限のプロジェクトを生成できるようになった
  • elm install をプロジェクトを初期化せずに elm make で全部やるようになった

elm-package.json が elm.json になった

アプリケーション用とライブラリ用で書き方が変わる。 アプリケーションの場合はバージョンが固定されて lock ファイルになる。 test-dependencies という項目ができたのでテスト用にもう一つ JSON を用意する必要がなくなった。 初期状態でインストールされるのは elm/coreelm/json の2つで、 HTML などは手動でインストールする必要がある。

elm-stuff の役割が変わった

パッケージやそのビルド生成物は ~/.elm にキャッシュされることになった。 CI とか Docker の設定で注意が必要かもしれない。

ユーザー定義の演算子が禁止された

もう作れない。

いくつかの演算子が消えた

  • ! はもうない
  • %modByremainderBy になった

トリッキーな関数が消えた

  • flip
  • curry
  • uncurry

人類には早かった。

シャドーイングが禁止された

同スコープに定義されている関数と同名のローカル変数を作ることができない。

公式リポジトリが elm-lang から elm になった。

ついでにバージョンが 1.0.0 にリセット。

Browser が登場

Html.program 系の関数がここに集約された。 Navigation, Mouse, Keyboard, Dom もここに統合された。 Navigation 相当の機能は fullscreen の時のみ使用可能。

Html.Events のデコーダーが柔軟になった

「デコードした時に特定の条件だった時に preventDefault しない」のようなことが可能になった。

DOM のイベントを同期的に実行するようになった

ブラウザのセキュリティ機能で、ユーザーインタラクションの直後じゃないとコピペなどが正しくハンドリングされない問題を解消した。

Debug モジュールの刷新

  • Debug.crash が Debug.todo になった。
  • toString が Debug モジュールに入った。

String.fromInt, String.fromFloat

toString は使わない。 型を変えた時に人知れず表示がバグっていたので嬉しい。

(,) が消えた

今は Tuple.pair

Html.Lazy が 8 引数まで拡張された

今まで3だった。

Array, Dict, Set の実装が新しくなった

速くなった。 Array は今までバグがあった。

Random の実装が新しくなった

PCG アルゴリズムを使うようになった。

Time の実装が新しくなった

より実用的になって elm/time として生まれ変わった。

Color が core から消えた

旧石器時代からあったやつ。

Regex が core から消えた

Parser を使って欲しそう。

elm-tools/parser が elm/parser に昇格

API も色々刷新された。

再帰的な関数のバグが直った

どれのことだったか忘れた。

リテラルで巨大なリストを作れないバグが直った

[1,2,3, ... 5800 ] みたいなやつ。

Module.Record.property を正しくパースするようになった

定数を使う時に苦労していたので地味に嬉しい。

エラーメッセージがさらに改善された

どれのことだったか忘れた。

Native -> Kernel

Native モジュールが Kernel モジュールに名称変更。 JS を書くのが許されているのは elm と elm-exploration の2つだけ。

ドキュメントが充実した

公式サイト・ガイド、コアライブラリの解説が増えた。

Union Type という表記が消えた

名称が紛らわしかった。今は Custom Type と書いてある。

個人的によく使う npm ライブラリを紹介してみる

偏ってます。

もっと有名なのは沢山あるけど、自分が普段よく使うのじゃないと紹介できないので。

argv

引数をパースするやつ。

chalk

色をつけるやつ。

dotenv

環境変数をファイルから読むやつ。

fs-extra

fs に欲しいけどないやつ。

watch

ファイルを監視してコマンドを実行するやつ。

nodemon

ファイルを監視してサーバーを再起動するやつ。

mocha

テスト。

prettier

フォーマッター。

puppeteer

ヘッドレス Chrome

express

サーバー。

passport

OAuth 。

typescript

TypeScript 。

elm

Elm 。

npm

Node.js 用のパッケージマネージャー。