プログラムは同じ入力なら同じ結果になると思いがちですが、実際には時刻や乱数、ネットワークなどの影響で結果が変わることがあります。
本記事では、初心者でも分かるように決定性(予測できる動き)と非決定性(毎回変わり得る動き)の違いと、予測しやすいコードにするコツを順を追って解説します。
決定性と非決定性とは
プログラムの挙動と予測の基本
入力・状態・出力をそろえると予測しやすい
プログラムの結果は、与えられたデータだけでなく、時刻やOS、ファイルの中身などの「状態」にも左右されます。
同じ入力と同じ状態であれば、同じ出力と同じ副作用になるとき、私たちはその挙動を予測できると言えます。
逆に、状態が毎回少しずつ違うと結果も揺れてしまいます。
予測できるとは何か
ここでの「予測できる」とは、説明できる・再現できるという意味です。
テストを何度実行しても同じ結果になり、バグ報告を受けても手元で同じ症状を再現できる状態が理想です。
再現できれば直せるため、予測のしやすさは開発効率に直結します。
決定性とは
一言でいうと
決定性とは、同じ入力と同じ条件なら結果が常に同じになる性質です。
例えば、2つの数の足し算や、固定の文字列連結は決定的です。
プログラム外部の揺らぎを取り除けているほど、決定性は高まります。
もう少しだけ補足
決定的なコードは原因と結果の対応がはっきりしており、仕様を文章化しやすくテストもしやすいです。
副作用が少ない小さな関数ほど決定的になりやすいのが実務的な感覚です。
非決定性とは
一言でいうと
非決定性とは、同じ入力に見えても毎回結果が変わり得る性質です。
例えば現在時刻に依存した処理や、乱数、ネットワーク越しのAPI応答、複数スレッドの実行順などが原因になります。
何が難しいのか
非決定的なコードは、テストが「たまに落ちる」などの症状を引き起こします。
これにより、原因を特定しづらく、修正の見通しが悪くなります。
「たまたま通る・たまたま落ちる」状態は最も危険です。
プログラミング初心者が理解する理由
初心者ほど、手元での再現性が学習スピードを左右します。
決定性を意識すると、学びやすく、バグの原因を絞り込みやすくなります。
まずは決定的な土台を作り、必要な範囲だけ非決定的な要素を扱う、という順番が理解への近道です。
決定性の例とメリット
例: 足し算や文字列操作は予測しやすい
基本の処理は決定的
固定の入力に対する数値計算や文字列結合は、毎回まったく同じ結果になります。
例えば「2+3」はいつでも「5」ですし、「”A”+”B”」もいつでも「”AB”」です。
外部環境に依らない処理は基本的に決定的です。
例: 並び替えが固定なら結果も一定
ソート条件を具体的に決める
「名前の昇順」のように並び替え条件がはっきりしていれば、同じデータは毎回同じ順序になります。
ただし、同じキーのときの並びが定義されていないと順序が揺れます。
同順位のときのタイブレーク条件(例: IDで昇順)も明記すると安定します。
注意点
連想配列やハッシュマップの走査順は言語や実装で保証されないことがあります。
表示や比較の前に明示的にソートすることで、結果を固定できます。
メリット: 再現性とテストのしやすさ
決定的なコードは、失敗の再現と原因の説明が簡単になります。
テストデータと期待値を固定すれば、何度実行しても同じ結果が得られます。
再現性は品質と開発スピードの共通通貨です。
| 場面 | 決定性が生む良い効果 |
|---|---|
| デバッグ | 症状をすぐ再現でき、修正が早い |
| コードレビュー | 変更の理由と影響範囲が説明しやすい |
| テスト | 落ち着いたテストスイートで信頼度向上 |
| 本番運用 | 障害時に原因が追いやすく復旧が速い |
コーディングのコツ
小さく分けて副作用を減らす
計算だけを行う関数と、入出力を行う関数を分けると、前者は決定的になります。
「計算は純粋、外部とのやり取りは端に寄せる」と覚えると実践しやすいです。
依存を引数で渡す
時刻や乱数、設定などを関数の中で直接取得せず、引数として受け取ると、テスト時に固定値を渡せます。
「隠れた依存」をなくすことで決定性が高まります。
非決定性の例と原因
例: 時刻や日付に依存するコード
何が起きるか
現在時刻やタイムゾーンに依存すると、日付境界や夏時間の切り替えなどで結果が変わります。
テストの実行時間帯で落ちたり通ったりする典型例です。
例: 乱数(random)を使う処理
何が起きるか
毎回違う値が出るため、並び順や選択結果が揺れます。
セキュリティ用途の乱数は特に扱いに注意で、テスト用の乱数とは分けて考える必要があります。
例: APIやネットワークの応答
何が起きるか
応答時間、失敗率、エラー内容が環境やタイミングで変わります。
外部サービスは自分で完全に制御できないため、そのままでは非決定的になりやすいです。
例: 入力の順序が決まらない処理
何が起きるか
ディレクトリのファイル一覧、ハッシュマップの走査、イベント到着順などは、順序が固定されていないことがあります。
順序に依存した結果は揺れます。
例: 並行処理(スレッド)の実行順
何が起きるか
スレッドやタスクのスケジューリングはOS任せです。
共有データの更新順が変わると、競合状態や取りこぼしが発生し、結果が安定しません。
例: OSや環境の違いで変わる動き
何が起きるか
ファイルの改行コード、大小文字の扱い、ロケール依存の並びなどが異なり、手元では通るのにCIでは落ちるといった現象が起きます。
| 原因 | 例 | よくある症状 | 対処の方向 |
|---|---|---|---|
| 時刻・日付 | 現在時刻の参照 | 日付境界でテストが落ちる | 固定時刻を渡す、タイムゾーンを固定 |
| 乱数 | ランダム抽選 | 並びや選択が揺れる | seed固定、テスト用PRNGを使用 |
| ネットワーク | 外部API | たまにタイムアウト | モック化、リトライ・タイムアウトを明示 |
| 入力順 | マップ走査 | 並びが毎回違う | 明示的にソート、タイブレーク定義 |
| 並行処理 | 複数スレッド更新 | 競合で値が不定 | 排他や順序付け、キュー化 |
| 環境差 | OS・ロケール | ローカルとCIで差異 | 設定固定、コンテナ化による再現 |
予測と制御のコツ
非決定性を減らす基本チェックリスト
次の観点をひと通り確認すると、揺れの多くを早期に抑えられます。
- 時刻、乱数、環境変数、ロケールは固定できますか
- 入力データの順序は明示的に決めていますか
- 外部APIはテスト時に置き換えられますか
- 並行処理の共有データは一貫した順序で更新していますか
- 例外やエラーの扱いが一定ですか
- ログに条件(時刻、seed、設定)が残りますか
チェックリストは「毎回同じ条件で動かせているか」を確かめる道具です。
乱数のseedを固定する
テストでは乱数の初期値(seed)を固定し、毎回同じ乱数列を得ます。
これによりデータの並びや抽選結果が安定します。
セキュリティや本番の重要な抽選で同じseedを使い回すのは避けるべきで、テスト専用の設定に留めます。
本番は安全性、テストは再現性と役割を分けましょう。
時刻は固定値を渡す
現在時刻を関数の中で直接取得せず、引数として日時を受け取ります。
テストでは固定の日時を渡し、本番では現在時刻を渡すだけで、同じロジックのまま決定性を確保できます。
タイムゾーンやカレンダー規則も設定で固定するとさらに安定します。
入力の順序を明示的にソート
処理前にソートしてから渡すと、順序に依存した結果が安定します。
同順位のときの並びも決めるとよいです。
例えば「名前昇順、同じならID昇順」のように二段階の並び替えを決めると、微妙な揺れを防げます。
共有データの更新は順序を決める
複数の処理から共有データを触る場合は、更新順や方法を統一します。
1つのキューに入れて順番に処理する、まとめてスナップショットを差し替えるなど、「同じ順序で同じ手順」を徹底すると再現性が上がります。
例外やエラーを明確に扱う
例外の握りつぶしは非決定性の温床です。
エラー時の戻り値やメッセージ、リトライ回数や待機時間などの挙動を明確にし、「失敗したときにどうなるか」を仕様に含めます。
これにより、テストも運用も読みやすくなります。
ログで条件と結果を残す
後から再現するために、seed、固定時刻、タイムゾーン、アプリや設定のバージョンなどの条件をログに出力します。
「どんな条件でその結果になったか」が残っていれば、バグの再現と修正がぐっと楽になります。
決定性を高めるテストの作り方
テストでは、固定の入力と期待値、固定の乱数seed、固定の時刻を用意します。
外部APIやストレージはテスト用の代替実装に差し替え、純粋な計算部分を単体で検証します。
さらに、比較が難しい複雑な出力は、整形した同一順序の表現(例: キー順でソートしたJSON文字列)に変換して比較すると安定します。
たまに落ちるテストへの対処
原因が非決定性にある場合が多いです。
まずはログから条件(seed、時刻、環境)を特定し、手元で同条件を再現します。
そのうえで、「固定できるものは固定する」「順序は決める」「外部依存は置き換える」の三点を適用します。
やむを得ず外部依存を残すときは、失敗時のメッセージを具体化し、リトライやタイムアウトを曖昧にしないことが重要です。
まとめ
決定性は「同じ条件なら同じ結果」という再現性の約束であり、学習、開発、テスト、運用のすべてを安定させます。
反対に、非決定性は時刻、乱数、ネットワーク、順序、並行処理、環境差などから簡単に入り込みます。
初心者のうちは、時刻と乱数を固定し、入力をソートし、外部依存を分離するだけでも、大きく予測しやすくなります。
今日から「隠れた依存を引数に出す」「順序を決める」を意識して、落ち着いたコードとテストを手に入れてください。
