JavaScriptのIIFEについて
IIFEとは
以下のようなコードがあったとします。
declare const str: string;
try {
const json = JSON.parse(str);
console.log('parsed: %o', json);
} catch {
console.log('not json');
}
このコードはつまり、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);
}
こうすれば、後続の処理は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');
}
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);
}
これらの違いは、後続の処理、ここでいえば 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;
}
})();
これを三項演算子で無理やり書くこともできるでしょうが、こちらの書き方には次のようなメリットがあるでしょう。
- 複数の選択肢から分岐する、という処理に対し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);
}
ところで、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);
}
また、この例のように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(',', ' ');
以下のように書き換えられます。
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;
})();
言語仕様レベルでどうにかならなかったのか
さて、文が式であるという言語は割とあります。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))
}
さらに言えば三項演算子でも 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のようなライブラリを利用したり、関数として切り出したりする必要があるかもしれません。
-
このケースに関して言えばneverthrowのfromThrowableを使うなどや、それに類似したヘルパー関数を使うのはあると思います ↩︎
-
https://en.wikipedia.org/wiki/Immediately_invoked_function_expression ↩︎
-
Expressions https://262.ecma-international.org/14.0/#sec-ecmascript-language-expressions ↩︎
-
Statements and Declarations https://262.ecma-international.org/14.0/#sec-ecmascript-language-statements-and-declarations ↩︎
-
FunctionExpression https://262.ecma-international.org/14.0/#prod-FunctionExpression ↩︎
-
ArrowFunction https://262.ecma-international.org/14.0/#prod-ArrowFunction ↩︎
-
https://stackoverflow.com/questions/18830626/should-i-use-big-switch-statements-in-javascript-without-performance-problems#comment27798508_18830724 ↩︎
-
DebuggerStatement https://262.ecma-international.org/14.0/#prod-DebuggerStatement ↩︎
-
JavaScriptからletを絶滅させ、constのみにするためのレシピ集 https://qiita.com/kiyoshiro/items/13c60fad1f5279993fa2 ↩︎
-
マクロ等を宣言できる場合は文もあるかもしれないが、それでもトップレベルにのみ配置できることが多いだろう。 ↩︎
-
https://doc.rust-lang.org/reference/expressions/match-expr.html ↩︎
世界のラストワンマイルを最適化する、OPTIMINDのテックブログです。「どの車両が、どの訪問先を、どの順に、どういうルートで回ると最適か」というラストワンマイルの配車最適化サービス、Loogiaを展開しています。recruit.optimind.tech/
Discussion