JavaScriptのIIFEについて

2024/02/18に公開

IIFEとは

以下のようなコードがあったとします。

declare const str: string;
try {
  const json = JSON.parse(str);
  console.log('parsed: %o', json);
} catch {
  console.log('not json');
}

[TS playground]

このコードはつまり、JSONとしてパースしてみて、JSONなら parsed: <その内容>, そうでないなら not json とコンソールに書くコードです。

JSONとして解釈可能な文字列かどうかで分岐したいだけなのだけれど、 JSON.parse のような、成功するかどうかをthrowするかどうかでしか判定できないものは、try-catchで囲う必要がやはりありますよね。[1]

たとえば、パースできない場合は空のオブジェクト ({}) にフォールバックしたい、みたいなケースでも(そのようなフォールバックは多くの場面ですべきではないでしょうが)やはりtry-catchを使うしかないです。(もしくは別でJSONパーサーを用意するか)

しかしこの書き方だと、jsonに対して追加の処理をしたいときに、どうしてもtry文の中に追記していくことになってしまいます。
そこで以下のように書き換えます。

declare const str: string;
const parsed = (() => {
  try {
    return {
      ok: true as const,
      json: JSON.parse(str),
    };
  } catch {
    return {
      ok: false as const,
    };
  }
})();

if (!parsed.ok) {
  console.log('not json');
} else {
  console.log('parsed: %o', parsed.json);
}

[TS playground]

こうすれば、後続の処理はtry-catchを抜けたあとで行えますね。例えば、 delete parsed.json.SECRET をしてからコンソールに書き出したいとしましょう。

最初の書き方

declare const str: string;
try {
  const json = JSON.parse(str);
  delete json.SECRET;
  console.log('parsed: %o', json);
} catch {
  console.log('not json');
}

[TS playground]

IIFEと合わせたときの書き方

declare const str: string;
const parsed = (() => {
  try {
    return {
      ok: true as const,
      json: JSON.parse(str),
    };
  } catch {
    return {
      ok: false as const,
    };
  }
})();

if (!parsed.ok) {
  console.log('not json');
} else {
  delete parsed.json.SECRET;
  console.log('parsed: %o', parsed.json);
}

[TS playground]

これらの違いは、後続の処理、ここでいえば delete parsed.json.SECRET などでthrowされた際の挙動です。実際、 str = 'null' だった場合などにこれらの違いは現れます。
try に全部入れていると余計なエラーまで握りつぶしてしまいますね。

この例はPythonでいえばtry-else節に似ています

Pythonではif以外でもelse節を取ることができる構文が多いです。tryもそのうちの一つです。tryの構文配下のようになっています。

try_stmt  ::=  try1_stmt | try2_stmt | try3_stmt
try1_stmt ::=  "try" ":" suite
               ("except" [expression ["as" identifier]] ":" suite)+
               ["else" ":" suite]
               ["finally" ":" suite]
try2_stmt ::=  "try" ":" suite
               ("except" "*" expression ["as" identifier] ":" suite)+
               ["else" ":" suite]
               ["finally" ":" suite]
try3_stmt ::=  "try" ":" suite
               "finally" ":" suite
try:
  parsed = parse_something(text)
except:
  print("not parsed")
else:
  print("parsed", parsed)
  # ...

このとき、else句に書いた処理はエラーとして正しくraiseされ飛んでいきます。重要なのは、変数スコープが引き継がれているという点です。

***

IIFEはImmediately Invoked Function Expression[2]の略です。

  • Immediately: 即時に
  • Invoked: 実行される
  • Function: 関数
  • Expression: 式

ということで即座に実行される関数式、即時実行関数式なんて言ったりします。

式(expression[3])と文(statementとdeclaration[4])が分かれているJSのような言語では、しばしば文を式にしたいことがあります。(後半でもっと例を見ていきます)
そういった場合に利用できるのがIIFEです。

なお、関数式(Function Expression)というと (function() { /* ... */ }) の構文[5]だけを指しそうですが、アロー関数(Arrow Function)[6]などを即時実行することも含みます。実際アロー関数を使ったほうが this を再束縛しないですし、なにより多少簡潔になります。

IIFEでできること

if/swtichを式のように使う

まず基本的な例として以下のような例を考えてみます。

type MyResponse = {
  isError: true;
  type: 'internal' | 'auth' | 'notFound' | 'invalid';
} | {
  isError: false;
  content?: string;
};
declare const response: MyResponse;
const code = (() => {
  if (response.isError) {
    switch (response.type) {
      case 'internal':
        return 500;
      case 'auth':
        return 401;
      case 'notFound':
        return 404;
      case 'invalid':
        return 400;
    }
  } else {
    if (typeof response.content !== 'string') return 201;
    if (response.content.length > 1e5) return 413;
    return 200;
  }
})();

[TS Playground]

これを三項演算子で無理やり書くこともできるでしょうが、こちらの書き方には次のようなメリットがあるでしょう。

  • 複数の選択肢から分岐する、という処理に対しswitch文を使うのは見やすく、switchの一般のメリットも得られる(scrutineeを一度だけ書けば良い、V8による最適化[7]、変更しやすい、など)
  • デバッグしやすい。プリントを挟んだり debugger [8] を差し込んだり

ところでこの例は少し微妙です。
そもそも、このような処理は私であれば関数として切り出してください、といいます。さらに言えば、プロジェクトによってはts-pattern[9]を導入してそちらを利用するようにしています。実際に、現在の業務でのチームではそうしています。

他にもReactのコンポーネント内で式の埋め込みをするときに使いたくなることは多いでしょう。以下で擬似的な例を見ています。

const MyComponent = ({ richContent }) => {
  return <div>
    {
      (() => {
        switch (richContent.type) {
          case 'text':
            return <span>{richContent.text}</span>;
          case 'icon':
            return <Icon icon={richContent.icon} />;
          case 'quote':
            return <blockquote>{richContent.quote}</blockquote>;
        }
      })()
    }
  </div>;
};

このパターンでも私は基本的にコンポーネントを分けるように言いますが、このくらいの例なら実際にありそうですね。
ただ、やはり同様にexhaustivenessをチェックするという意味でも基本的にはts-patternをチーム開発では選ぶと思います。

try-catchを式のように使う

冒頭の例でも紹介した通り、try-catchを囲うことで式のように扱う(成功する場合と失敗する場合での場合分けのように扱える)ことができて、さらに、tryする範囲を必要最小限にすることができます。
こちらは割とあります。

letをconstへ

try-catchの例と似てますが、例えば外部のAPIで、存在を確かめるために get が成功するか失敗するかでしか確かめることができない状況を考えてみます。

declare const api: {
  get: (id: string) => Promise<{ id: string, name: string }>;
};

async function main() {
  let exists = false;
  try {
    await api.get('xyz');
    exists = true;
  } catch {}
  console.log('exists: ', exists);
}

[TS Playground]

ところで、letはconstにしたいですよね[10]
この例ではそこまででもないですが、大きな処理の中でこれがあったら、letの変化がないかを追うのは大変になります。

以下のように書き換えられます。

async function main() {
  const exists = await ((async() => {
    try {
        await api.get('xyz');
        return true;
    } catch {
        return false;
    }
  }))();
  console.log('exists: ', exists);
}

[TS Playground]

また、この例のようにasync/awaitの場合でもIIFEは使えます。

他にも、なんども再代入をするようなケースでも使えます。

declare const input: string;
declare const capitalizeWords: (s: string) => string;

let s = input;
s = s.trim();
s = capitalizeWords(s);
s = s.replaceAll(',', ' ');

[TS Playground]

以下のように書き換えられます。

declare const input: string;
declare const capitalizeWords: (s: string) => string;

const s = (() => {
    let s = input;
    s = s.trim();
    s = capitalizeWords(s);
    s = s.replaceAll(',', ' ');
    return s;
})();

[TS Playground]

言語仕様レベルでどうにかならなかったのか

さて、文が式であるという言語は割とあります。Lisp/ML系はすべて式といえることが多いでしょう[11]。Rustもトップレベル以外ではすべて式です。

そういった取り組みがまさにdo式のプロポーザルです。しかし、残念ながらこのプロポーザルは何年もStage 1のままです。
Issueを見てもわかるようにこのプロポーザルには問題がいくつかあり、個人的な所感としても入ることはないだろうと思っています。(同時に、現在のプロポーザルの方針のまま入ることは嬉しくないと思っています)

もう一つ関連した候補があります。それがパイプライン演算子のプロポーザルです。これはIIFEが必要な箇所の一部について解決しえます。特に、#letをconstへの再代入を何度もするケースで利用できる可能性のあるものです。

declare const input: string;
declare const capitalizeWords: (s: string) => string;

const s = input
  |> %.trim()
  |> capitalizeWords
  |> %.replaceAll(',', ' ')

こちらも Stage 2 ではあるので、まだまだ先は長そうですが...

IIFEでは解決しないこと

ここでは、言語レベルで各種構文が式になっている言語(特にRustと比較して)では起きない、IIFE特有の問題を紹介します。

IIFEでは解決しないこと: IIFE内で外の関数のreturnができない

rustでは式の中でreturnすることができます。 IIFE ではできないですし、もっと言えば三項演算子でもできないですね。

Rustではたとえば

fn f(n: u32) -> u32 {
    let a = if n == 0 {
        return 0
    } else {
        n - 1
    };
    a + 1
}

fn main() {
    println!("{}", f(0))
}

[Rust Playground]

さらに言えば三項演算子でも flag ? return 123 : 0 と書けるわけではないです。Rustはreturnもボトム型 (never 相当) を返す式となっている、とも言えるでしょうか

IIFEでは解決しないこと: ループ・swtichのbreakができない

やはり同様に、Rustではbreakは式の中でも行える(ちなみに他言語のswitch相当のmatch式[12]にbreakはないです)ものですが、関数の中だと break の文脈は途切れてしまいます。

実際にどれくらい使っているか?

例えば私の現在の業務のプロジェクトでは5万行ほどTSのコードがありますが、12箇所ありました。 (rg '\)\(\)' で検索して調査)

特に、複数のステップを単に表現するバッチ処理で関数自体は長めのものの、constのみにしたい場合や、 *.config.ts 系で完結に書くために利用するなどの用途でした。

IIFEを使うべきか?

IIFEは必ずしも使うべきということにはならないと思いますが、constではなくlet(そして再代入)が使われている。特に大きなスコープの中で使われているケースでは暫定的対処としては使えるでしょう。更にその先としては、ts-patternやfromThrowableのようなライブラリを利用したり、関数として切り出したりする必要があるかもしれません。

脚注
  1. このケースに関して言えばneverthrowのfromThrowableを使うなどや、それに類似したヘルパー関数を使うのはあると思います ↩︎

  2. https://en.wikipedia.org/wiki/Immediately_invoked_function_expression ↩︎

  3. Expressions https://262.ecma-international.org/14.0/#sec-ecmascript-language-expressions ↩︎

  4. Statements and Declarations https://262.ecma-international.org/14.0/#sec-ecmascript-language-statements-and-declarations ↩︎

  5. FunctionExpression https://262.ecma-international.org/14.0/#prod-FunctionExpression ↩︎

  6. ArrowFunction https://262.ecma-international.org/14.0/#prod-ArrowFunction ↩︎

  7. https://stackoverflow.com/questions/18830626/should-i-use-big-switch-statements-in-javascript-without-performance-problems#comment27798508_18830724 ↩︎

  8. DebuggerStatement https://262.ecma-international.org/14.0/#prod-DebuggerStatement ↩︎

  9. https://github.com/gvergnaud/ts-pattern ↩︎

  10. JavaScriptからletを絶滅させ、constのみにするためのレシピ集 https://qiita.com/kiyoshiro/items/13c60fad1f5279993fa2 ↩︎

  11. マクロ等を宣言できる場合は文もあるかもしれないが、それでもトップレベルにのみ配置できることが多いだろう。 ↩︎

  12. https://doc.rust-lang.org/reference/expressions/match-expr.html ↩︎

GitHubで編集を提案
OPTIMINDテックブログ

Discussion