async / await への感謝を綴る ── 継続・CPS・ダイレクトスタイルを素人なりに辿る
はじめに
JavaScriptやTypeScriptにおいて、非同期処理を同期処理のように直感的に記述できる async/await 構文は、現代のフロントエンド開発において欠かせない機能の1つとなっています。
普段ほとんど何も考えずに書いている const user = await fetchUser(1); という一行ですが、この機能が当たり前のように使えるようになるまでには、計算機科学の世界での長い積み重ねがあったようです。
きっかけは、TSKaigi 2026 でのおーみー氏の発表「『関数型プログラミング』を分解する.ts」1 を聴き、継続(Continuation)や継続渡しスタイル(Continuation Passing Style, CPS)という概念に興味を持ったことでした。
当記事では、計算機科学や圏論の専門家ではない私が素人なりに調べた範囲で、この async/await の背後にある歴史的なつながりと、TypeScript の具体例を通じた個人的な気づきを共有することを試みます。
継続とは何か
CPSの話に入る前に、まずは「継続」という言葉の意味を確認しておきたいと思います。
Wikipedia「継続」2 における説明は次の通りです。
計算機科学における継続(けいぞく、continuation)とは、プログラムを実行中のある時点において、評価されていない残りのプログラム(the rest of the program)を表現するものであり、手続き(procedure)あるいは関数(function)として表現されるものである。
実際に、継続を簡単なコードで確認してみましょう。
const x = 1 + 2;
const y = x * 10;
console.log(`answer = ${y}`);
このコードでは、const x = 1 + 2; の継続とは次の部分にあたります。
const y = x * 10;
console.log(`answer = ${y}`);
このように、ある時点での継続とは、その時点から先の処理全体を指します。
ここでもう一歩踏み込んで、この継続を関数として書き出してみましょう。
const x = 1 + 2; のあとに続く処理を、「x を受け取って残りを実行する関数」として切り出すと、次のように書けます。
const k = (x: number) => {
const y = x * 10;
console.log(`answer = ${y}`);
};
const x = 1 + 2;
k(x); // x を、その時点の継続 k に渡す
意味としては元のコードとまったく同じですが、残りの処理が k という独立した関数として切り出されています。
このように、継続は「その時点の値を受け取って、残りの処理を行う関数」として書き表すことができます。
そして、「次に何をするか」を関数(継続)として受け取り、計算の最後にそれを呼び出すスタイルが、CPS(継続渡しスタイル / Continuation Passing Style) と呼ばれています。 一方、私たちが普段書いているような形は、ダイレクトスタイル と呼ばれているそうです。
早速、ダイレクトスタイルと CPS で同じ計算を並べて書いてみましょう。
// ダイレクトスタイル
const direct = (): number => {
const x = 1 + 2;
const y = x * 10;
return y;
};
// CPS
const cps = (k: (answer: number) => void): void => {
const x = 1 + 2;
const y = x * 10;
k(y); // 計算結果 y を継続 k に渡す
};
ダイレクトスタイルは普段通りで、特別な仕組みは何もありません。
一方の CPS は、「結果が出たら何をしてほしいか」を関数 k として受け取り、計算の最後に k(y) として呼び出すことで、継続に値を渡しています。
こうして見比べると、ダイレクトスタイルのほうが読みやすく、書き慣れた形ですよね。
しかし、このCPSの仕組みが日常的に活きていた例があります。
それが、TypeScriptでおなじみの Promise です。
Promise が CPS で書かれている理由
おなじみの Promise を使った非同期処理のコードを、改めて見てみましょう。
const fetchUser = (id: number): Promise<User> =>
fetch(`/users/${id}`).then((r) => r.json());
fetchUser(1).then((user) => {
console.log(user.name);
});
.then に渡している関数こそが継続で、これは先ほどの CPS と同じ形です。
ここで気になるのは、なぜ Promise は継続を渡す形になっているのか、という点です。
そもそも Promise<User> は、「いつかは User が取り出せるものの、今すぐには中身に触れられない」という状況を表す型です。
言い換えれば、User の上に「非同期処理が完了するまで待つ」という文脈が乗った型と言えます。
素人なりに考えてみると、Promise<User> のまま扱おうとするより、いったん User を取り出した世界で処理を書く方が、取り回しが良さそうです。具体的には次の 2 点です。
- Promise の重なり具合に巻き込まれずに済む: 非同期処理を連鎖させると、戻り値は素朴には
Promise<Promise<User>>のように積み上がる可能性があります。User単体だけを相手にするコードは、こうしたPromiseの重なり具合に依存せず、「Userをどう使うか」だけを書けば済みます。 - 非同期の完了タイミングを意識せずに済む:
Promise<User>の中身は、非同期処理が完了するまで存在しません。一方、Userを受け取って動くコードであれば、いつ呼び出されるかを気にする必要がなく、「Userが来た時点でやることだけ」を素直に記述できます。
CPS はまさに、この「素の値を取り出した後の世界で操作する」ことに重きを置いた設計だと言えそうです。
Promise<User> から User を取り出す責任は .then の側に渡し、書き手は「User を受け取ったあとに何をするか」を継続として記述するだけで済みます。
このように Promise は、「Promise<User> のまま値を扱わせる」のではなく、「いったん User を取り出した世界で処理を組み立てさせる」という設計を選んでいるのだろうと感じました。
.then のネストと async/await
とはいえ、CPS をそのまま書き続けると、扱いにくさが書き手のほうに跳ね返ってきます。
たとえば、Promise を返す API を .then で順に繋いでみると、次のようになります。
declare function getUser(id: number): Promise<User>;
declare function getPosts(userId: number): Promise<Post[]>;
declare function getComments(postId: number): Promise<Comment[]>;
declare function getAuthor(authorId: number): Promise<User>;
getUser(1).then((user) => {
getPosts(user.id).then((posts) => {
getComments(posts[0].id).then((comments) => {
getAuthor(comments[0].authorId).then((author) => {
console.log(`著者: ${author.name}`);
});
});
});
});
処理が進むほどインデントが右下にずれていき、いわゆるコールバック地獄と呼ばれる形になりました。
ですが現代の TypeScript では、同じ処理を async/await を使って次のように書けます。
const main = async (): Promise<void> => {
const user = await getUser(1);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
const author = await getAuthor(comments[0].authorId);
console.log(`著者: ${author.name}`);
};
インデントの深まりが消え、変数を上から下に書き連ねるだけのコードに戻りました。
これはまさにダイレクトスタイルそのものです。
非同期処理(Promise<User> を返す API)を扱っているにもかかわらず、書き手はあたかも同期処理のように、User を直接受け取って処理を続けています。
非常に読みやすく、書きやすいコードになっています。
ダイレクトスタイルに至るまでの歴史
ここまで自然にダイレクトスタイルで書けるようになるまでには、何かしらの理論的な裏付けがあったはずです。それはどんな歴史を辿ってきたのか、私は気になりました。
少し調べてみたところ、計算機科学の世界では1980年代後半から、Promise のような「文脈を付与する仕組み」を モナド として捉え、CPS で書かれたコードをダイレクトスタイルでも書けるように整備する流れがあったそうです。
私たちが普段使っている Promise も、こうした研究の延長線上にある、モナドの一例なのだそうです。
私の辿れた範囲では、次のような研究の積み重ねがあったようです。
1989年: Eugenio Moggi 氏によるモナドの導入
モナドという概念をプログラム意味論の世界に持ち込みました3。この時点では、副作用を伴う計算を数学的に記述するための理論的な枠組みとしての位置づけだったようです。
1990〜1992年: Philip Wadler 氏による構文の整備
モナドを実用的なプログラミングへと応用し、Haskell の do 構文やモナド内包表記などの基礎を築きました45。開発者がダイレクトスタイルで書いたコードを、コンパイラが CPS へと変換する構文糖衣にあたります。
1992年: Olivier Danvy 氏による Back to Direct Style の発表
Wadler 氏とは逆方向の視点として、すでに CPS で書かれたプログラムを、ダイレクトスタイルへと逆変換するアルゴリズムを示した論文です6。
1994年: Andrzej Filinski 氏による Representing Monads の発表
言語に継続を動的に操作する機能(shift / reset)が組み込まれていれば、モナドをコンパイル時の書き換えに頼らず、ダイレクトスタイルのまま実行できることを示しました7。モナドはCPSをダイレクトスタイルに置き換えることができる、という理論的な位置づけを与えました。
上記は AI の補助を借りながら整理したメモです。論文の中身や理論的な位置づけを正確に追いかけるのは、私のような門外漢には難しすぎました。 気になった方は、ぜひ脚注の論文を直接あたってみてください。
振り返ってみれば、「継続を関数として切り出す」「Promise が CPS の形で値を受け渡す」「async/await でダイレクトスタイルに戻る」という今回辿った道のりも、これらの理論整備の延長線上にあるのだろう、と感じています。
そもそも、こうした歴史に踏み込むきっかけをくれたのは、TSKaigi 2026 でのおーみー氏の発表でした。「継続」「CPS」という言葉に出会わなければ、私は今日も await を便利な機能として書き続けていたのだろうと思います。
そして、モナドという枠組みの発見から、ダイレクトスタイルでも記述可能であることが理論として示されるまでの道のりを築いてくれたのは、Moggi 氏、Wadler 氏、Danvy 氏、Filinski 氏ら、先人たちでした。
こうして道のりを辿ってきたいま、きっかけをくれたおーみー氏と、これを築き上げてくださった先人たちに、改めて感謝したいと思います。
Footnotes
-
おーみー.「『関数型プログラミング』を分解する.ts」. TSKaigi 2026. https://2026.tskaigi.org/talks/3 ↩
-
Wikipedia「継続」. https://ja.wikipedia.org/wiki/%E7%B6%99%E7%B6%9A ↩
-
E. Moggi. Computational Lambda-Calculus and Monads. LICS 1989. (改訂版: Notions of Computation and Monads. Information and Computation, 1991.) ↩
-
P. Wadler. Comprehending Monads. In Proceedings of the 1990 ACM Conference on LISP and Functional Programming, 1990. ↩
-
P. Wadler. The Essence of Functional Programming. POPL 1992. ↩
-
O. Danvy. Back to Direct Style. In Proceedings of ESOP 1992. (改訂版: Back to Direct Style. Science of Computer Programming, Vol. 22, 1994. https://doi.org/10.1016/0167-6423(94)00003-W) ↩
-
A. Filinski. Representing Monads. In Proceedings of POPL 1994. https://dl.acm.org/doi/10.1145/174675.178047 ↩