はじめに
先日「関数型まつり2025」にて「成立するElixirの再束縛(再代入)可という選択 - Speaker Deck」という内容で発表させていただいたのですが、関連して、今回、いろいろな言語における値の不変性と変数の不変性について整理したいと思います。
値の不変性と変数の不変性
まず、しばしば混同されがちな「値の不変性」と「変数の不変性」の違いについてです。
値の不変性 (Immutable Value) こちらは、一度作成された値(データ)そのものが変更できない性質を指します。不変な値を変更しようとすると、実際には元の値を変更するのではなく、変更内容を反映した新しい値が作成されます。
変数の不変性 (Immutable Variable) これは、一度値を代入された変数に、別の値を再代入できない性質を指します。多くの言語で
const
(JavaScript, C++) やfinal
(Java) といったキーワードで実現されます。変数が指し示す「参照先」が固定されると考えると分かりやすいでしょう。変数の不変性は「再束縛(再代入)不可」と言い換えることもできます。
具体例:JavaScriptの const
JavaScriptのconst
は変数を不変にしますが、値まで不変にするとは限りません。
// 変数 'user' は不変(再代入不可) const user = { name: "Taro", age: 30 }; // エラー: userに別のオブジェクトを再代入しようとしている // user = { name: "Jiro", age: 25 }; // TypeError: Assignment to constant variable. // OK: userが指し示すオブジェクト(値)のプロパティを変更することは可能 user.age = 31; // user の中身は { name: "Taro", age: 31 } になる
この例では、user
という変数の参照先は変わりませんが、その参照先にあるオブジェクトの中身(値)は変更できてしまっています。これが、値の不変性と変数の不変性の違いになります。
各言語における「値の不変性」と「変数の不変性」の扱い比較表 📊
各言語での「値の不変性」と「変数の不変性」の取り扱いをまとめた表が下記になります。
言語 | 値の不変性 | 変数の不変性 | 備考・特徴 |
---|---|---|---|
Elixir | 不変 | 再束縛可 | = は代入ではなくパターンマッチ演算子。再束縛可だがデータは常に不変。 |
Erlang | 不変 | 再束縛不可 | 単一静的代入 (SSA)。一度束縛すると再束縛はできない。 |
Ruby | 混在 | 指定可 | 大文字で再束縛不可にできるが警告付きで再束縛可能。すべてがオブジェクトへの参照。 |
Scala | 混在 | 指定可 | 再束縛不可の val と再束縛可の var を明確に使い分ける。 |
Java | 混在 | 指定可 | final は再束縛を禁止。参照先のオブジェクト内部は変更可能。 |
TypeScript | 混在 | 指定可 | 再束縛不可の const と再束縛可の let を明確に使い分ける。 |
php | 混在 | 指定可 | const は再束縛を禁止。 |
Python | 混在 | 再束縛可 | 再束縛を禁止する構文はない。大文字の変数名で定数を示す慣習がある。 |
Go | 混在 | 指定可 | const で宣言できるのはコンパイル時に決定する値のみ。 |
Rust | 指定可(mut ) |
指定可(mut ) |
所有権システムが特徴。デフォルトで不変だが mut で値や変数を可変にできる。 |
Haskell | 不変 | 再束縛不可 | 純粋関数型言語。参照透明性を保つため、値や変数は常に不変。 |
言語ごとの特徴的な考え方
表の内容について、特に特徴的な言語をいくつかピックアップして補足します。
Erlang / Haskell: 安全性を重視するイミュータブル(不変)
Erlang と Haskell は、値や変数が イミュータブル(不変) で再束縛が不可になっています
この設計は、意図しない値の変更を防ぎ、コードの安全性を高めることを目的としています。Haskell のような純粋関数型言語では、この不変性がプログラムの正しさを保証するための根幹をなしています。
Erlang / Elixir: 同じ VM、異なるアプローチ
Erlang は、高い並行性と信頼性を実現するために、変数の再束縛を許さない 単一静的代入(SSA) を原則としています。
一方、同じ Erlang VM 上で動作する Elixir は、プログラマーの利便性を考慮して再束縛を許容しています。値自体は Erlang 同様にイミュータブルなため、再束縛しても元のデータが変更されることはなく、安全性は保たれています。
これは両言語の設計思想の違いを示す興味深い点です。
Scala / TypeScript: 選択肢を与えるハイブリッド型
Scala の val
/ var
や TypeScript の const
/ let
は、開発者に変数が不変か可変かの選択を委ねます。
// Scalaの例 val x = 10 // 再束縛できない (Immutable) // x = 20 // エラー var y = 10 // 再束縛できる (Mutable) y = 20 // OK
一般的なベストプラクティスは 「まず val
(または const
) を使い、どうしても必要な場合のみ var
(または let
で再束縛) を使う」 というものです。これにより、変更される可能性がある変数の範囲を最小限に抑え、コードの見通しを良くします。
Rust: デフォルトは不変だが可変の余地も提供
Rust はデフォルト挙動としては値は不変で変数の再束縛を許可していませんが、値や変数を変更したい場合は、mut
というキーワードで「この値や変数は変更を許可します」と明示的に宣言することで可能となります。
let x = 5; // イミュータブル (変更不可) // x = 6; // コンパイルエラー! let mut y = 5; // ミュータブル (変更可能) y = 6; // OK let mut numbers: Vec<i32> = vec![10, 20, 30]; // ミュータブル (変更可能) numbers.push(40); // OK
この設計は、意図しない値の変更を防ぎ、コードの安全性を高めることを目的としています。
Python / Ruby: 柔軟性を重視するすべてがオブジェクト参照
Python や Ruby では、変数は常にオブジェクトへの参照であり、再束縛はいつでも自由に行えます。再束縛を禁止する const
のような仕組みは言語レベルでは提供されていません。
# Pythonの例 my_list = [1, 2, 3] # リストオブジェクトの中身を変更 (再代入に近い) my_list.append(4) # 別のリストオブジェクトに再束縛 my_list = [10, 20]
この柔軟性は書きやすさにつながる一方、どこで変数の値が変わるか追いにくくなる側面もあります。そのため、Python コミュニティでは定数として扱いたい変数名をすべて大文字で書く(例: MAX_CONNECTIONS = 10
)という紳士協定(慣習)が生まれました。
まとめ
「値の不変性」と「変数の不変性」。これらの用語は、プログラミング言語が「状態」と「データ」をどのように扱うかという設計思想に深く関わっています。
- 値がイミュータブル(不変): 一度作られたデータの変更を許さない性質
- 変数がイミュータブル(不変): 名前の関連付け先の変更を許さない性質
近年のプログラミングでは、イミュータビリティを重視する傾向が強まっています。データが不変で再束縛が不可あれば、プログラムの動作が予測しやすくなり、特に並行処理など複雑な状況でのバグを劇的に減らすことができるからです。
ただ、Elixirでは、値がイミュータブル(不変)である一方、変数は再束縛可という一風変わったアプローチを採用しています。 これは上記傾向に反し、悪手のようにも見えますが、データが不変であることや、関数型言語でありスコープ等が限定的に機能していることにより、コードの視認性を損なわずに利便性を享受できる、Elixirならではの個性的な設計思想になっています。
Elixirの再束縛可というアプローチに興味を持たれた方は、ぜひ発表スライドやYouTube動画をご視聴いただければ嬉しいです。
また、今回の関数型まつりでは会社ブースも出展していて、当日のレポートが下記になります。よかったらこちらもご覧いただければ。