TypeScriptやJavaでif式やtry式が欲しい時

概要

TypeScriptやJavaなど、 iftry が式ではなく文になっている言語でそれらを式として使う方法の一つとして即時実行関数式を紹介します。 また、専用のユーティリティ関数によるアプローチと比較します。

対象とする問題

TypeScriptやJavaでは iftry が文であるため、宣言的な記述が難しい場面があります。

// 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 関数は単純ながら意外にも多機能です。

  • iftryswitch などに対応している
  • 遅延評価に対応している
  • スコープを作り、変数宣言ができる

などなど。単純に関数に備わっている機能が使用できます。

実装が単純明快である

iife の実装は関数を実行するだけであり、非常に理解しやすいです。

このことは地味に見えますが非常に強力で、バグったときに何が起きるのかを追うのが非常に簡単です。

ユーティリティ関数は一度作ればずっと使いまわせるのでそのような場面は少ないように思われますが、 iftryswitch それぞれに実装が必要になったり、それらに遅延評価を追加したくなって機能変更をする場面など、実装・デバッグが必要な場面は案外何度か発生してしまいます。

静的コード解析が適用されやすい

iife 関数は宣言された関数を実行するだけなので、人間だけでなく静的コード解析にとっても理解しやすくなっています。

例えば、JavaでOptionalな値をget()するコードパスでisPresent()をチェックしているかをコード解析ツールが追うことができます。

さらに、TypeScriptの場合は if の条件によるType Guardも有効になります。

まとめ

式志向で書きたい時、ついif式を実現するためのユーティリティを書きたくなってしまいますが、即時実行関数式によるアプローチも案外強力であると思います。

参考