TypeScriptで使えるMaybeモナドライブラリの比較

小飼です。
TypeScript
の2系が正式に公開されました。
多数の機能追加・改善がありましたが、中でも目玉の一つはstrictNullChecks
ではないでしょうか。
今まで曖昧だった、『ある型がnull/undefinedを取り得るか?』ということを、型レベルで厳密に検査・検出することができるようになる機能です。
実行時エラーになるバグの多くが、不用意なundefined/nullへのアクセスから生じることからも、これがアプリケーションの堅牢性に大きく寄与する機能であることが伺えます。
JavaScript has two values for “emptiness” – null and undefined. If null is the billion dollar mistake, undefined only doubles our losses. These two values are a huge source of errors in the JavaScript world because users often forget to account for null or undefined being returned from APIs.
例えば従来のTypeScript
では、以下のようなInnerFoo
がnull
を取り得るコードであっても、問題なくコンパイルが通っていました。
// (旧)実行時エラーになるが、Validなコードだった
interface InnerFoo {
buzz: number;
}
interface OuterFoo {
bar: InnerFoo;
}
const innerFoo = null;
const foo: OuterFoo = { bar: innerFoo };
console.log(foo.bar.buzz); // Uncaught TypeError: Cannot read property 'buzz' of null
strictNullChecks
を有効化していると、InnerFoo
を{ buzz: number } | null
と表現しない限り、const innerFoo = null;
のようにnull
を代入できません。
また、InnerFoo
を{ buzz: number } | null
と表現する限り、foo.bar.buzz
から直接値を取り出すこともできなくなります。
// (新)Foo.barがnullを『持ちうる』という文脈を型として明示しないとコンパイルエラーになるようになった
interface InnerFoo {
buzz: number;
}
interface OuterFoo {
bar: InnerFoo | null;
}
const innerFoo = null;
const foo: OuterFoo = { bar: innerFoo };
const buzz: number = foo.bar ? foo.bar.buzz : -1;
console.log(buzz);
これによって、いわゆるMaybeモナド(※)
で包んでおいた方が良さそうな型が検出される、とも捉えられるかと思います。
(ScalaとかSwiftではOption/Optional型として提供されていますね)
せっかくこの機能を適用するのであれば、生の文法通りT | null
と書くよりも、いい機会なのできちんと定義されたMaybeモナド
に包んできれいに使いたいものです。
※ すごく雑に説明すると『ある型の値を持っているかも知れない』型のこと
そこで本稿では、TypeScript
で書かれたMaybeモナド
を提供するライブラリをいくつか見てみて、その使い勝手を検証してみたいと思います。
monapt
Option
, Try
, Future
を提供する、インターフェースがちょっとScala
っぽいライブラリです。
import { equal } from "assert";
import { Option, None } from "monapt";
interface InnerFoo {
buzz: number;
}
interface OuterFoo {
bar: Option<InnerFoo>;
}
const innerFoo: Option<InnerFoo> = None;
const foo: OuterFoo = { bar: innerFoo };
// 値の取り出し
const buzz1: number = foo.bar
.getOrElse(() => ({ buzz: -1 }))
.buzz;
// パターンマッチ風の値取り出し
const buzz2 = foo.bar
.match({
Some: x => x.buzz,
None: () => -1,
});
equal(buzz1, -1);
equal(buzz2, -1);
TsMonad
Maybe
, Either
, Writer
を提供するライブラリで、JavaScriptにおける代数的データ構造の共通仕様を策定しているFantasyLandのインターフェースに準拠しているそうです(この辺かなり理解が怪しいのですが...)
/// <reference path="./node_modules/tsmonad/dist/tsmonad.d.ts" />
import { equal } from "assert";
import { Maybe } from "tsmonad";
interface InnerFoo {
buzz: number;
}
interface OuterFoo {
bar: Maybe<InnerFoo>;
}
const innerFoo: Maybe<InnerFoo> = Maybe.nothing<InnerFoo>();
const foo: OuterFoo = { bar: innerFoo };
// 値の取り出し
const buzz1 = foo.bar
.valueOr({ buzz: -1 })
.buzz;
// パターンマッチ風の値取り出し
const buzz2: number = foo.bar
.caseOf({
just: x => x.buzz,
nothing: () => -1,
});
equal(buzz1, -1);
equal(buzz2, -1);
monadness
この中では一番若いライブラリで、Either
, Option
, Tuples
を提供しています。
インターフェースはmonapt
に似ていますが、唯一パターンマッチ(風)メソッドの実装されていないライブラリでもあります。
まだ開発中のライブラリのようですので、今後実装されていく予定なのかも知れません。
import { equal } from "assert";
import { Option } from "monadness";
interface InnerFoo {
buzz: number;
}
interface OuterFoo {
bar: Option<InnerFoo>;
}
const innerFoo: Option<InnerFoo> = Option.none<InnerFoo>();
const foo: OuterFoo = { bar: innerFoo };
// 値の取り出し
const buzz1 = foo.bar
.getOrElse(() => ({ buzz: -1 }))
.buzz;
equal(buzz1, -1);
まとめ
TypeScript
で扱えるMaybeモナド
を提供するライブラリをいくつか紹介してみました。
JavaScript
には言語組み込みのパターンマッチが無いだけに、代替となるメソッドを生やしてプロパティ名でパターンマッチ風の動作を実現しているような実装にグッと来ました。
(当然細かいパターンの指定やパターンガードはできないので、Scala
やSwift
のパターンマッチと較べてしまうと機能的にかなり見劣りしてしまうのですが...)
さて、実際に簡単なコードを描いてみてMaybe
のみの使い勝手で言うと、どのライブラリにも大きな差は無いように思います。
この中から選ぶとすると、同時に提供している型に欲しいものがあるか、で選ぶことになるかも知れません。
個人的には、Maybe
モナドに余分な(filterとか)メソッドが生えてて便利そうなのと、インターフェースが最近かじっているScala
に似ていて読みやすいあたりでmonapt
を選びたいと思っています。(Either
が欲しいところですが、Try
がそれに相当する型として提供されているので)
簡単な内容でしたが、今回書いたコードはここにまとめておきました
compare-maybe-monad
以上、参考になればうれしいです。