ES5の範囲でOption<T>型を表すライブラリ、option-t を作った

動機

初期状態で未選択なラジオボタンがあるようなフォームを作っている場合、ラジオボタンに対応するモデルの値を「この値は未選択である」というのをJSで表現するのは結構面倒くさい。チェックボックスであれば, booleanのどちらかで状態が確定するが、ラジオボタンだと取りうる値は複数になるし、初期状態で選択されているか否かの問題が発生する。選択されていない状態を専用にフラグとして持つのは気持ち悪いが、かといって、未選択の状態を-19999ないしnullundefinedで表現するのは危うい。コードを書いた本人しかわからない。

RustやScalaなどのようにOption<T>/Maybeがある言語なら、こんなまどろっこしい思いはせずに、明示的に値の有無を表現できる。 というわけで、ないなら作ってしまえば良いじゃないメソッドで作った。

Option<T>型について

私が説明するよりもわかりやすいであろう先行の説明があるので、それを紹介する。

作ったもの

元々、仕事で書くコードの為に作ったものなので、もしかしたら会社のgithubアカウントの方に移管するかもしれない。

設計思想

  • Option<T> をECMA262 5thの範囲内で実装する
  • APIモデルについては、Rustのstd::optionをベースとした
  • 詩的な名前は思い浮かばなかったので質実剛健な名前にした
    • あまり文学的な名前にしても後で忘れる。
  • 主対象環境は、TypeScriptおよび生のES5

使い方

こんな感じ

var OptionT = require('option-t');

// `Some<T>`
var some = new OptionT.Some(1);
console.log(some.isSome); // true
console.log(some.unwrap()); // 1

// `None`
var none = new OptionT.None();
console.log(none.isSome); // false
console.log(none.unwrap()); // this will throw `Error`.

おなじみのmap(), flatMap()の他に、値を取り出す為のunwrap()、デストラクタ代わりのdrop()あたりを実装している。最終的には、Rustのstd::optionAPIのほとんどを実装したいところ。

Some<T>NoneインスタンスOption<T>であることを、どう表現するか?

これが一番悩んだ。最初はOptionTypeのような一つのオブジェクトを用意し、コンストラクタに渡す引数がundefinedか否かを確認するようにしていたんだけど、「正常系でundefinedを返すケースでも、暗黙的にNoneに変換されてしまう」ため、断念して、結局別個のオブジェクトを作る事になった( OptionTypeについては移行期間中のため、deprecated扱い)

ここで、それぞれのオブジェクトがOption<T>であることを表現する為に、Option<T>というインターフェースを用意して、TypeScript向けにはそれぞれがOption<T>を実装していると見せることにした。

一方、生のJS向けにはOptionTというオブジェクトをプロトタイプチェーンの祖先に突っ込んで(基底クラスに突っ込んで)、option instanceof OptionTで確認できるようにした。が、これについてはあくまでも型システムにインターフェースを所持しない環境向けであり、TypeScriptのようにインターフェースが普通に使える環境には露出させていない。

Promise<T>Maybeモナドのように使うのではダメだったか

2015年の我々には、モナドと非同期コンテナを混ぜ込んだ、Promise<T>という共通インターフェースがある. これを使うのも考えたが、少なくともES5にはawaitに相当する仕組みが無く, Promise<T>では値を同期的に取り扱うことができないので止めた。RxなどのObservable<T>についても同様。同期的に扱えるコンテナが欲しかった

なぜES5? ES6でもbabelでも良いじゃん?

ES5で十分に書ける範囲の内容なので、transpilerは(要らないと)判断した. そのうち気が向いたらTypeScriptやbabelに移行するかもしれないけどね。

browserifyありきなんですが

browserifyありきですね

先行事例

作った後に気がついたんだけど、極めて類似の先行事例にopty (npm)があった。こちらはRustのAPIモデルをベースにTypeScriptで書いたもの。

先にこっちに気がついていればoptyを使う可能性もあったけれど、テストコードが無かったのと、JSONシリアライズ時の表現が特に記述されていなかったのと、RustのそれよりもAPIが(無駄に)増えていたので、音楽性の問題で当面は別個に開発するつもり。将来的には、あっちに合流して大統合してもいいかなとは感じる

まとめ

感想お待ちしております