はじめに:ifとforで「完全に理解した」気になっていたあの頃
私がプログラミングの世界に足を踏み入れたのは、高校時代の競技プログラミング(競プロ)がきっかけでした。
当時の私の武器は、四則演算と if 文、そして for ループ、そして便利なライブラリだけ。しかし、それさえあれば複雑な探索アルゴリズムも書けるし、実行時間も速い。「プログラミングって、要は条件分岐と繰り返しの組み合わせなんだ。意外とシンプルじゃん」と、考えていました。
しかし大学に入り、Web開発の世界──特にReactやJavaScriptのコードに初めて触れた時、違和感を感じることになります。
「なぜ、処理をわざわざ『関数』にしてボタンに渡すんだ?」
onClick={handleClick} のように、関数を変数のように扱って投げ渡す書き方。あるいは、突然登場する「クラス(Class)」という謎の概念。「if と for があれば全部シンプルに上から下へ書けるのに、なぜわざわざ処理を関数に包んでたらい回しにしたり、クラスなんていう複雑な枠組みに閉じ込めたりするんだ?」
当時の私には、モダンなWeb開発の書き方が「単純なことをわざわざ難しく書いているだけの、無駄な複雑化」にしか見えませんでした。
そこで本記事では、過去の私がずっと抱えていた「なぜこんな書き方をするのか?」というモヤモヤを解消すべく、プログラミング言語の設計思想の変遷を「歴史的な文脈」から紐解いてみることにしました。
前提:パラダイムは「上書き」ではなく「地層」である
本題に入る前に、前提を共有しておきます。
プログラミングのパラダイム(設計思想)は、新しいものが古いものを完全に駆逐してきたわけではありません。用途に応じた「適材適所」として、現在も共存しています。
- 手続き型(C言語など): 実行速度やメモリ効率が重視される分野(競技プログラミング、OS・組み込み開発など)で現在も主流。
- オブジェクト指向(Javaなど): 大規模で長期的な保守が必要な業務システムの開発において有効。
- 関数型・イベント駆動(React、モダンWebなど): 非同期処理や複雑なUIの状態管理が求められる現代のWeb開発で普及。
なぜWeb開発では「関数を渡す」という書き方が普及したのか。その背景を、1960年代の構造化プログラミングから順を追って見ていきます。
1. 1960〜70年代:GOTO文の課題と「構造化プログラミング」
現代の私たちが当たり前のように使っている if や for ですが、プログラミング言語の歴史において、これらは最初から標準的な存在だったわけではありません。
初期のプログラミング(アセンブリ言語や初期のBASICなど)では、処理の流れを制御するために主に GOTO 文 が使われていました。これは「指定した行番号に強制的にジャンプする」という、非常に自由度が高く強力な命令です。
スパゲティコードの誕生
プログラムが数十行程度のうちは、GOTO文による制御でも問題なく動いていました。しかし、1960年代後半にかけてソフトウェアが大規模化し、コードが数千行、数万行に膨れ上がると、致命的な問題が発生します。
コードのあちこちから任意の場所にジャンプできるため、「今、どの変数がどういう処理を経てこの行に辿り着いたのか」という実行の文脈を、人間が追跡できなくなってしまったのです。処理の糸が複雑に絡み合ったこの状態は 「スパゲティコード」 と呼ばれ、当時のソフトウェア開発における最大の課題となりました。
ダイクストラの提唱と「構造化プログラミング」
このカオスな状況を打破したのが、計算機科学者のエドガー・ダイクストラらによって提唱された 「構造化プログラミング」 というパラダイムです。
彼は1968年に「GOTO文は有害である」という有名な書簡を発表し、どれほど複雑なアルゴリズムであっても、以下の 3つの基本構造だけで記述できる(記述すべきである) という原則を打ち立てました。
- 順次(Sequence):上から下へ、書かれた順番に処理を実行する。
-
反復(Iteration):条件を満たす間、処理を繰り返す(
for,while)。 -
分岐(Selection):条件によって処理を分ける(
if,switch)。
これにより、プログラムは「上から下へと一定の方向に流れる」という予測可能な形を取り戻し、バグの発生を抑え、他人が読んでも理解しやすい(保守性の高い)コードへと進化しました。
競プロと構造化プログラミングの親和性
ここで注目したいのは、この「順次・反復・分岐」を駆使する手続き型・構造化プログラミングのアプローチが、競技プログラミングのような領域では現在でも最適解であるという事実です。
競プロで求められるのは、「一度与えられた入力データに対し、最短時間で計算を行い、結果を出力してプログラムを終了させる」ことです。このような目的において、余計な関数の受け渡しや状態の管理(オブジェクトの生成など)を行わず、for と if だけでCPUに直接的な命令を下す書き方は、実行速度やメモリ効率の面で最も優れています。
しかし、実行環境が「CLI」から「Webブラウザ(GUI)」へと移り変わることで、この前提が根底から覆ることになります。
2. なぜWeb開発では「ifとfor」だけだと破綻するのか?
前節で触れたように、CLI(コマンドラインインターフェース)環境では「順次・反復・分岐」の手続き型アプローチが最強です。なぜなら、CLIプログラムでは 「プログラム自身が主導権を握っている」 からです。プログラムは入力が与えられるまで待機し、与えられた瞬間に一気に計算を終わらせて終了します。
しかし、実行環境が「Webブラウザ(GUI)」に変わると、この前提が完全に崩れ去ります。
主導権はプログラムではなく「ユーザー」にある
Webブラウザの世界では、プログラムに主導権はありません。ユーザーが「いつ」画面をスクロールし、「いつ」ボタンをクリックするかは完全に予測不可能です。さらに、サーバーにデータを要求した場合、「いつ」その返事が返ってくるかもネットワークの状況次第です。
もし「ifとfor」でボタンクリックを実装したら?
この環境で、ボタンが押されたかどうかを従来の for(while)と if だけで検知しようとするとどうなるでしょうか。コードのイメージは以下のようになります。
// 【破綻するコードのイメージ(ポーリング)】
while (true) {
if (ボタンが押されたか?) {
// ボタンが押された時の処理
break;
}
}
この「常に変化を監視し続ける」手法をポーリングと呼びます。
一見すると論理的に正しいように見えますが、これをWebブラウザ上で実行すると 画面が完全にフリーズします。 なぜなら、CPUが「ボタンが押されたか?」という確認作業(無限ループ)にかかりきりになってしまい、画面の描画や文字の入力といった他の処理を一切行う余力がなくなってしまうからです。このように、ある処理が終わるまで他の処理が止まってしまう状態を 「ブロッキング」 と呼びます。
パラダイムシフト:「イベント駆動」と非同期のメカニズム
このブロッキング問題を回避し、画面のフリーズを防ぎながらユーザー操作を検知するために取り入れられたのが 「イベント駆動(Event-Driven)」 というアーキテクチャです。
Webブラウザ上で動くJavaScriptは、原則として 「シングルスレッド」 で動作します。つまり、同時に1つの作業しか実行できません。この制約の中でイベント駆動を実現する中核となる仕組みが 「イベントループ(Event Loop)」 です。
ブラウザ内部では、主に以下の4つの要素が連携して非同期処理を実現しています。
-
メインスレッド(Call Stack):
JavaScriptのプログラムが順次実行される場所です。ここは常に「今すぐ実行できる処理」だけを担当し、重い処理や「待ち」が発生する処理をここに留まらせることはできません。 -
Web APIs(ブラウザのバックグラウンド):
ボタンのクリック監視やタイマー(setTimeout)、外部への通信(fetch)といった「いつ発生・完了するかわからない処理」は、メインスレッドから切り離され、ブラウザの裏側のシステムに監視が委譲されます。 -
タスクキュー(Task Queue):
裏側のシステムで「ボタンが押された」「通信が完了した」などのイベントが発生すると、それに紐づけられていた「実行すべき処理」が、このキュー(待ち行列)に順番に格納されます。 -
イベントループ(Event Loop):
メインスレッドが空いているかどうかを常に監視し続ける仕組みです。メインスレッドの作業が完了して空きができると、タスクキューに並んでいる処理を1つ取り出し、メインスレッドに送り込んで実行させます。
「関数を渡す」ことの厳密な意味
このメカニズムを踏まえて、モダンなWeb開発におけるボタンクリックのコードを見てみましょう。
最もシンプルな「ボタンを押したら Hello World と表示する」という実際のコードを見てみましょう。HTMLに <button id="btn">クリック</button> というボタンがあるとします。これをJavaScriptでイベント駆動にするコードは以下のようになります。
// 1. 実行してほしい処理(関数)を定義する
function sayHello() {
console.log("Hello World!");
}
// 2. 画面からボタンの要素を取得する
const button = document.getElementById("btn");
// 3. ボタンに「クリックされたら sayHello を実行してね」と関数を「渡す」
button.addEventListener("click", sayHello);
このコードの3行目(addEventListener)が実行された時、ブラウザの裏側では以下の手順が進行しています。
- メインスレッドは「
clickイベントが起きたら、sayHelloという手順書を実行してね」という指示をWeb APIsに登録し、自身の作業を即座に終了します。これによりメインスレッドが解放され、画面のフリーズを回避します。 - ユーザーがいつクリックするかは、ブラウザの裏側(Web APIs)が監視し続けます。
- ユーザーがボタンをクリックした瞬間、ブラウザは登録されていた
sayHello関数をタスクキューに格納します。 - イベントループがそれを検知し、メインスレッドが空いたタイミングで
sayHelloが実行されます。
ここで注目すべき最も重要なポイントは、3行目で関数を渡す際、sayHello() のように末尾にカッコ () をつけていないという点です。
手続き型言語に慣れていると、関数には必ずカッコをつけるのが当たり前のように感じられます。しかし、ここでカッコをつけてしまうと全く別の意味になってしまいます。もし addEventListener("click", sayHello()) と書いてしまうと、その行を読み込んだ瞬間にメインスレッドで sayHello が即時実行されてしまい、「クリックされたら後で実行する」という予約(Web APIsへの登録)ができなくなってしまいます。
-
sayHello()(カッコあり): 「今すぐここで実行して、その結果(戻り値)を頂戴」という即時実行の命令。 -
sayHello(カッコなし): 「この『処理の手順書』そのものをデータとして渡すから、後であなたのタイミングで実行して」という委譲。
Web APIsに監視を依頼し、後からタスクキューに格納して実行してもらうためには、「今すぐ実行する」のではなく、「処理そのもの」をデータのように持ち運び、引数として引き渡せる形式にしておく必要がありました。このように、関数を変数に代入したり引数として渡したりできる言語の性質を「第一級オブジェクト」と呼びます。
3. 2010年代〜現在:「How」から「What」へのシフト
イベント駆動とコールバック関数によって、Webブラウザは画面をフリーズさせずに操作を捌けるようになりました。しかし、Webアプリが複雑化するにつれ、フロントエンドの開発現場は新たな限界に直面します。
「どう書き換えるか(How)」を命令する限界
従来のJavaScript(例えばjQueryが主流だった時代)では、画面を更新する際に「DOM(画面の要素)を直接操作する」というアプローチをとっていました。「ボタンをクリックしたら通信を開始し、その間はボタンを赤色の『送信中』にする。通信が終わったら元の青色のボタンに戻す」 という一連の処理を、第2節までの「命令型」の書き方で実装してみます。
// 【命令型UIの書き方(従来のJavaScript)】
const button = document.getElementById("submit-btn");
button.addEventListener("click", async function() {
// 1. 通信開始前:開発者が「どう画面を書き換えるか」を1つずつ命令する
button.textContent = "送信中...";
button.classList.remove("blue");
button.classList.add("red");
button.disabled = true;
// 2. 通信処理を実行
await fetch('/api/submit', { method: 'POST' });
// 3. 通信完了後:再び手動で画面を「元の状態」に書き換えて戻す
button.textContent = "送信する";
button.classList.remove("red");
button.classList.add("blue");
button.disabled = false;
});
このように、コンピュータに対して 「どうやって画面を書き換えるか(How)」 を1ステップずつ命令する書き方を「命令型プログラミング」と呼びます。
この手法の最大の欠点は、UIの書き換えと通信ロジックが密結合してしまうことです。通信が終わった後、手動で「色を戻す」「テキストを戻す」という逆の命令を書き忘れると、永遠にボタンが押せなくなるバグが発生します。これが複雑に絡み合うと、かつてのGOTO文が引き起こした「スパゲティコード」の再来となってしまいます。
パラダイムシフト:「どうあるべきか(What)」を宣言する
この状態管理の複雑さを解決するために普及したのが、Reactなどのモダンフレームワークが採用している 「宣言的UI(Declarative UI)」 というパラダイムです。
宣言的UIでは、「要素の色を変える」「テキストを書き換える」といった命令(How)を一切書きません。代わりに、「システムの状態(State)」と、「その状態のとき、画面はこうなるべき(What)」という完成図を完全に分離して記述します。
先ほどと等価な処理をReactで書くと、以下のようになります。
// 【宣言的UIの書き方(React)】
import { useState } from 'react';
function SubmitForm() {
// 1. システムの状態(State)を定義する
const [isSubmitting, setIsSubmitting] = useState(false);
// 2. ロジック:通信処理と「状態の変更」だけを行う
const handleSubmit = async () => {
setIsSubmitting(true); // 状態を「送信中(true)」に変更
await fetch('/api/submit', { method: 'POST' });
setIsSubmitting(false); // 状態を「通常(false)」に戻す
};
// 3. UI:状態に応じた「完成図」だけを宣言する(DOMの直接操作はしない)
if (isSubmitting) {
return <button className="red" disabled>送信中...</button>;
}
return <button className="blue" onClick={handleSubmit}>送信する</button>;
}
このコードでは、DOMを直接操作する記述が完全に消えています。handleSubmit 関数の中には通信処理と 「状態の変更(setIsSubmitting)」 しか書かれていません。
リアクティブ(反応的)プログラミングの浸透
この宣言的な設計を裏で成立させているのが、 「リアクティブプログラミング」 の思想です。
リアクティブとは「反応する」という意味です。handleSubmit によって状態(isSubmitting)が更新されると、フレームワーク側がその変化を検知し、宣言された完成図の通りに画面(DOM)を自動で書き換えてくれます。
私が大学で初めてReactに触れた際、見慣れない構文に戸惑ったのは、無駄に複雑化されているからではありません。「通信してDOMを書き換える(How)」という手続き型の思考から、「状態(State)を管理し、それに応じたUI(What)をマッピングする」というリアクティブな思考への切り替えを求められていたからです。
まとめ:パラダイムは「適材適所」の道具箱
競技プログラミングからWeb開発の世界へ足を踏み入れたとき、私が感じた「なぜこんな複雑な書き方をするのか」という違和感の正体は、プログラムが実行される「環境」の違いと、それに伴う「パラダイム(設計思想)の衝突」でした。
これまでの歴史を振り返ると、それぞれの書き方には明確な存在理由があることがわかります。
-
手続き型・命令型(if と for):
一度の入力に対して最短で計算を行う、CLIや競プロの環境において現在でも「最適解」です。 -
イベント駆動(コールバック関数):
いつ発生するかわからないユーザー操作を待ちつつ、ブラウザをフリーズ(ブロッキング)させないための、シングルスレッド環境における「必然の仕組み」です。 -
宣言的UI・リアクティブ(Reactなど):
複雑に絡み合うUIと通信ロジックを分離し、「状態の不整合(バグ)」を防ぐための、現代のWeb開発における「防波堤」です。
モダンなWeb開発のコードは、決して単純なことをわざわざ難しく書いているわけではありません。予測不能なユーザー操作や非同期通信が飛び交う環境において、システムを破綻させないために先人たちが生み出した合理的なルールなのです。