TypeScriptやJavaでif式やtry式が欲しい時
概要
TypeScriptやJavaなど、 if
や try
が式ではなく文になっている言語でそれらを式として使う方法の一つとして即時実行関数式を紹介します。
また、専用のユーティリティ関数によるアプローチと比較します。
対象とする問題
TypeScriptやJavaでは if
や try
が文であるため、宣言的な記述が難しい場面があります。
// TypeScript (手続き的な記述) let x; if (a > 0) { x = a; } else { x = -2 * a; }
// Rust (宣言的な記述) let x = if a > 0 { a } else { -2 * a };
この例は簡単すぎて条件演算子( a > 0 ? a : -2*a
)で実現できますが、条件が増えたりすると読みづらいと言われていますし、tryの場合はそのような文法はありません。
即時実行関数によるアプローチ
即時実行関数式を使えば、式になっていない制御構文を式として使用することができます。
// TypeScript const x = (() => { if (a > 0) return a; return -2 * a; })();
このままでは関数の宣言と読み間違えるリスクがあるので、次のようなユーティリティ関数を宣言することで意図を明確にできます。
// TypeScript // Immediately Invoked Function Expression: 即時実行関数式 const iife = <T>(f: () => T): T => f(); const x = iife(() => { if (a > 0) return a; return -2 * a; });
tryも式のように使えます。
// TypeScript const y = iife(() => { try { return parse(s); } catch(error) { return ''; } });
専用のユーティリティ関数との比較
ユーティリティ関数を作るアプローチ
if
式や try
式が欲しいとき、次のようにそれぞれの専用ユーティリティ関数を作るというアプローチもあります。
// TypeScript const x = if_(a > 0) .then(a) .else_(-2 * a); const y = try_(() => parse(s)).unwrap_or(-2 * a);
私自身もこのような実装は好きですし、下記のような利点があると思います。
- 意図の理解しやすさと文字数の少なさの両立
- かっこいい
これに対し即時実行関数式は平凡、野暮、冗長に見えますが、即時実行関数式にも明確で強力な次のメリットがあります。
機能が多い
iife
関数は単純ながら意外にも多機能です。
if
、try
、switch
などに対応している- 遅延評価に対応している
- スコープを作り、変数宣言ができる
などなど。単純に関数に備わっている機能が使用できます。
実装が単純明快である
iife
の実装は関数を実行するだけであり、非常に理解しやすいです。
このことは地味に見えますが非常に強力で、バグったときに何が起きるのかを追うのが非常に簡単です。
ユーティリティ関数は一度作ればずっと使いまわせるのでそのような場面は少ないように思われますが、 if
、 try
、 switch
それぞれに実装が必要になったり、それらに遅延評価を追加したくなって機能変更をする場面など、実装・デバッグが必要な場面は案外何度か発生してしまいます。
静的コード解析が適用されやすい
iife
関数は宣言された関数を実行するだけなので、人間だけでなく静的コード解析にとっても理解しやすくなっています。
例えば、JavaでOptionalな値をget()
するコードパスでisPresent()
をチェックしているかをコード解析ツールが追うことができます。
さらに、TypeScriptの場合は if
の条件によるType Guardも有効になります。
まとめ
式志向で書きたい時、ついif
式を実現するためのユーティリティを書きたくなってしまいますが、即時実行関数式によるアプローチも案外強力であると思います。