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では、以下のようなInnerFoonullを取り得るコードであっても、問題なくコンパイルが通っていました。

1
2
3
4
5
6
7
8
9
10
11
12
// (旧)実行時エラーになるが、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から直接値を取り出すこともできなくなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// (新)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

monapt

Option, Try, Futureを提供する、インターフェースがちょっとScalaっぽいライブラリです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

TsMonad

Maybe, Either, Writerを提供するライブラリで、JavaScriptにおける代数的データ構造の共通仕様を策定しているFantasyLandのインターフェースに準拠しているそうです(この辺かなり理解が怪しいのですが…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// <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

monadness

この中では一番若いライブラリで、Either, Option, Tuplesを提供しています。
インターフェースはmonaptに似ていますが、唯一パターンマッチ(風)メソッドの実装されていないライブラリでもあります。
まだ開発中のライブラリのようですので、今後実装されていく予定なのかも知れません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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には言語組み込みのパターンマッチが無いだけに、代替となるメソッドを生やしてプロパティ名でパターンマッチ風の動作を実現しているような実装にグッと来ました。
(当然細かいパターンの指定やパターンガードはできないので、ScalaSwiftのパターンマッチと較べてしまうと機能的にかなり見劣りしてしまうのですが…)

さて、実際に簡単なコードを描いてみてMaybeのみの使い勝手で言うと、どのライブラリにも大きな差は無いように思います。
この中から選ぶとすると、同時に提供している型に欲しいものがあるか、で選ぶことになるかも知れません。

個人的には、Maybeモナドに余分な(filterとか)メソッドが生えてて便利そうなのと、インターフェースが最近かじっているScalaに似ていて読みやすいあたりでmonaptを選びたいと思っています。(Eitherが欲しいところですが、Tryがそれに相当する型として提供されているので)

簡単な内容でしたが、今回書いたコードはここにまとめておきました
compare-maybe-monad

以上、参考になればうれしいです。

このエントリーをはてなブックマークに追加