プログラムがうまく動くかどうかは、データが今どんな状態かと、関数が外にどんな影響を与えるかで大きく変わります。
まずは用語をやさしく整理し、小さな例でイメージをつかみましょう。
状態と副作用を分けて考えることが、読みやすく壊れにくいコードへの最短ルートです。
プログラミングの状態(ステート)の基礎
状態(ステート)とは
日常のたとえ
冷蔵庫の中身や、目覚まし時計の設定のように、時間によって変わるものを想像してください。
これらは「今どうなっているか」という状況を表し、まさに状態です。
状態とは時間とともに変化する「今この瞬間の情報」です。
プログラムでの定義
プログラムでは、変数の中身やUIのオンオフ、ログイン中かどうかなど、実行中に覚えておく必要がある情報を状態と呼びます。
プログラムが記憶し続ける情報が状態です。
値と状態の違い
直感的な違い
値は「1や’Hello’のようにそれ自体で完結したデータ」で、見ても触っても変わりません。
一方、状態は「プログラムが保持する現在の値の集合」で、時間や操作で変化します。
値は不変のデータ、状態は変わりうる場所と考えると理解しやすいです。
一覧で比較
| 観点 | 値 | 状態 | 例 |
|---|---|---|---|
| 変化するか | 変わらない | 変わる | 数値3 vs カート内の商品数 |
| いつ決まるか | 作られた時点 | 実行の途中で変わる | 定数の文字列 vs 入力フォームの内容 |
| 同じ入力で同じか | 常に同じ | 変わることがある | 足し算の結果 vs 現在時刻の取得 |
| 保存場所 | 計算結果そのもの | メモリやストレージ上の位置 | イミュータブル文字列 vs ローカルストレージ |
値は結果そのもの、状態は保管場所とその中身の移り変わりです。
変わるデータと変わらないデータ
可変(ミュータブル)とは
可変データは後から中身を書き換えます。
配列に要素を追加したり、オブジェクトのプロパティを書き換えたりする操作がこれに当たります。
可変データは便利ですが、変更の追跡が難しくなりやすいです。
不変(イミュータブル)とは
不変データは作られたら変わりません。
変更したい時は新しいデータを作ります。
文字列や、変更せずに新しい配列を返すやり方が代表例です。
不変にすると「いつ変わったか」を気にせず安心して扱えます。
使い分けのコツ
基本は不変を選び、どうしても性能や必要性があるところだけ可変にします。
複数の場所で共有するデータほど不変にすると安全です。
不変を標準、可変を例外とするとコードが安定します。
状態の例
アプリでよく見る状態
- ログイン中かどうかのフラグ
- ショッピングカートの中身
- 画面のタブの選択状態
- 入力フォームの現在の文字
- キャッシュ済みの検索結果
日常的なアプリは、目に見えるほとんどの動きが状態に支えられています。
小さなワークフロー例
Todoアプリでは「未完了のタスク一覧」「入力中の文字」「フィルタ条件」などが状態で、ユーザー操作に応じてこれらが更新されます。
どの操作がどの状態を変えるかを明確にすると設計が楽になります。
プログラミングの副作用の基礎
副作用とは
日常のたとえ
電気のスイッチを押すと部屋の明かりが点くように、自分の手元以外に影響が及ぶことがあります。
これが副作用のイメージです。
自分の外側に変化を起こす行為が副作用です。
プログラムでの定義
関数の戻り値以外の変化、例えば画面を更新する、ファイルに書き込む、ネット通信を行う、グローバル変数を変える、といった動きが副作用です。
戻り値以外で世界を変えることが副作用です。
関数の入出力と副作用の違い
純粋な関数とは
同じ入力なら常に同じ出力を返し、外部に影響を与えません。
足し算やソートのような処理がこれに当たります。
純粋な関数は予測しやすくテストが簡単です。
副作用を持つ関数とは
ログを出す、DOMを更新する、APIを呼ぶなど、呼ぶたびに外で何かが変わります。
結果が環境やタイミングに左右されやすくなります。
副作用を持つ関数は便利ですが扱いに注意が必要です。
比較表
| 項目 | 純粋な関数 | 副作用を持つ関数 |
|---|---|---|
| 同じ入力の再現性 | ある | ないことがある |
| 外部への影響 | なし | あり |
| テスト容易性 | 高い | 低くなりやすい |
| 使いどころ | 計算や変換 | I/Oや表示、保存 |
計算は純粋に、外とのやりとりは副作用として切り分けるのが基本です。
よくある副作用の例
画面描画やDOM操作
ボタンのラベルを書き換える、リストを描き直すなどはユーザーの見える世界を変えます。
UIの更新は典型的な副作用です。
ファイルやネットワーク
ファイル保存、データベースへの書き込み、HTTPリクエストの送受信は外部資源に影響します。
永続化や通信は副作用の中心です。
時刻や乱数の取得
現在時刻や乱数は呼ぶたびに結果が変わるため、関数の出力が一定になりません。
時間や乱数は見かけ上の入力がなくても結果が揺れます。
ログ出力
コンソールや監視にメッセージを残す行為も外部の状態を増やします。
ログは開発に有用ですが副作用である点を意識します。
グローバル状態の変更
アプリ全体から見える変数や設定を書き換えると、別の場所の動作にも影響が及びます。
広く共有される状態の変更は影響範囲が大きいです。
副作用で起きやすい問題
予測しづらさとテストの難しさ
副作用は順序やタイミングで結果が変わるため、再現が難しくなります。
同じ操作で毎回同じ結果にならないという不安定さがテストを難しくします。
競合や取り合い
複数の処理が同じ資源に同時に触ると、思わぬ上書きや取り違えが起きます。
同時実行の競合は副作用の代表的な落とし穴です。
隠れた依存
目に見えないところで状態に依存していると、修正の影響を読み間違えます。
関数の外にある前提は文書化しないとバグの原因になります。
状態と副作用の関係と違い
状態の変化と副作用のつながり
状態そのものは概念ですが、状態を書き換える行為は副作用です。
逆に、状態を読み取るだけなら基本的には副作用ではありません。
「状態を変える」は副作用、「状態を読む」は副作用ではないのが基本です。
計算だけの関数と外の世界に触れる関数
計算だけの関数は入力を受け取り、出力を返すだけです。
外の世界に触れる関数は表示や保存などを行います。
計算と影響を分けると理解と保守が楽になります。
役割の分担のイメージ
計算関数は「何を表示するか」を決め、表示関数は「どう見せるか」を担当します。
何をとどうを分離すると再利用性が上がります。
例: カウンタ更新と画面表示の分離
下の例では、数を増やす計算は純粋関数に任せ、画面更新と保存は副作用の関数に集めています。
更新のロジックと表示の副作用を分けるのがポイントです。
# 純粋: 次のカウントを計算
def next_count(current, step):
return current + step
# 副作用: 画面に表示
def render_count(count):
ui_label.set_text(f"{count}") # 画面を変更
# 副作用: 保存
def save_count(count):
storage.write("count", count) # 外部に書き込み
# イベント処理: 役割を組み合わせ
def on_click():
c = storage.read("count") # 外部から読み取り
n = next_count(c, 1) # 純粋な計算
render_count(n) # 表示
save_count(n) # 保存
純粋関数はテスト、副作用関数は実運用にフォーカスと分けると扱いやすいです。
分けて考えるメリット
理解しやすく、バグの原因が見つけやすくなります。
純粋な部分だけを単体テストで素早く確認でき、外部の都合に左右されにくくなります。
分離は読みやすさとテスト容易性を同時に高めます。
実務上の効果
仕様変更が来ても計算と表示のどちらか一方だけを直せばよい場面が増えます。
影響範囲を小さく保てるので保守コストが下がります。
初心者向けの設計と管理のコツ
状態を少なく保つ
状態は必要最小限にし、計算で求まるものは保存しないようにします。
例えば合計金額はカートの中身から毎回計算します。
「唯一の正しい情報源」を決めて派生は計算で補うと混乱が減ります。
スコープを小さく
グローバルではなく、できるだけ関数やモジュール内に閉じ込めます。
状態の見える範囲が狭いほど安全です。
副作用は一箇所に集める
表示、保存、通信などは入口と出口にまとめ、普段のロジックから遠ざけます。
副作用の集約はトラブル発生時の切り分けを簡単にします。
例: ゲートウェイを用意
API呼び出しは専用の関数に集め、他の場所はその関数を呼ぶだけにします。
外部との接点を一本化すると差し替えやすくなります。
テストしやすい関数にする
時間や乱数、現在時刻など環境に依存する値は引数で受け取るようにします。
テスト時は固定値を渡せます。
外部依存を引数にすると関数はいつでも同じ動きをします。
小さな単位で検証
純粋関数を小さく保ち、入出力だけを確認できるようにします。
小さな単位は壊れにくく直しやすいです。
注意ポイント
非同期の副作用は順序が前後することがあります。
結果を前提にした処理は完了を待つ仕組みを入れましょう。
順序の思い込みはバグの温床です。
引数の書き換えに注意
受け取った配列やオブジェクトを直接変更すると、呼び出し元に影響します。
必要なら新しいデータを作り直します。
共有データの直接変更は避けるのが安全です。
例外や失敗の扱い
通信や保存は失敗することがあります。
失敗時の振る舞いを決めておくと安心です。
失敗も正常なケースとして用意しておくと堅牢です。
まとめ
状態はプログラムが覚えている「今の情報」で、副作用は戻り値以外で外に与える影響です。
両者を区別し、計算と影響を分けて設計すると、理解しやすくテストしやすい構成になります。
状態は少なく、副作用は一箇所に、計算は純粋にという原則が初心者にとって最強の指針です。
