値の不変性
Last Updated: 2026-09-28 02:00
結論
melangでは、すべてのデータを不変(イミュータブル)とする。不変にするかどうかを、書き手に選ばせない。
- 変数への再代入を禁止する。
- オブジェクトのフィールド変更を禁止する。
- コレクションの破壊的変更(要素の追加・削除・置き換えなど)を禁止する。
- 値を変えたいときは、変更後の新しい値を生成する。
if/for/ early return / I/Oなどの手続き的な処理そのものは、禁止しない。
let user = User("alice")
user = User("bob") // NG: 再代入
user.name = "bob" // NG: mutation
user.tags.push("admin") // NG: 破壊的変更
let renamed = user.withName("bob") // OK: 新しい値を生成
withNameは、更新を新しい値の生成として表す例である。更新の書き方(構文)は、このnoteでは決めない。
束縛と値の両方を不変にする理由
final(Java)・val(Kotlin)・const(TypeScript・JavaScript)は、変数の束縛を固定するだけで、参照先の値までは不変にしない。final List<String> tagsでも、tags.add("admin")は通る。
melangは、「束縛の再代入禁止」と「値そのものの変更禁止」の両方を言語仕様にする。片方だけでは、「この変数は変わらない」と読み取れても、「この値は変わらない」とは読み取れないからである。
不変を既定にせず、変更を選ぶ手段も設けない理由
Rust・Kotlin・Swiftのように「不変を既定にして、mut / varで変更を選べる」設計も考えられる。melangは、変更を選ぶ手段そのものを設けない。
- 変更を選べると、書き手が可変と不変を使い分けることになり、防御的コピー(呼び出し側や受け取り側で、念のため値を複製すること)が必要な場面が生まれる。
- 変更を選べると、ある値が不変かどうかを、型や書き方から確認しなければならない。
- 例外を設けないことで、「melangの値は変わらない」と、言語全体で言い切れる。
この保証は、「関数の引数」で決めた「引数は常に共有して渡す」の根拠でもある。呼び出された関数が引数を書き換えられないので、コピーしなくても、呼び出し側の値は変わらない。
手続き的な処理は禁止しない理由
melangが不変にするのはデータであり、if・for・early return・I/Oのような手続き的な処理は禁止しない。禁止しないと、手続き的な書き方が使える。
不変性と手続き的な処理は別の軸である。問題の原因は、共有された値が書き換わることであり、処理を順に書くことではない。手続き的な処理を残すことで、書き手が慣れた制御構文を使い続けられる。
このnoteで決めないこと
- ループ内の累積(
sum += xのような書き方)を、どう表現するか。再帰・fold・ループ構文が値を返す形などがあり得る。 - 値の更新を表す構文。
withNameのようなメソッド、コピーして一部を変える構文などがあり得る。 - I/Oや状態を伴う処理を、型などで区別するか(HaskellのIOのような設計を取るか)。
- 新しい値を作るコストを抑える実装。永続データ構造更新しても更新前のデータが壊れず、新旧の版が共存できるデータ構造。変更されなかった部分を新旧で共有するため、値を丸ごとコピーせずに、変更後の新しい値を効率よく作れる。HaskellやClojure、Scalaの不変コレクションなどで使われる。を使うか、コンパイラの最適化に任せるか。
- 参照の同一性(同じ値かどうか)を、言語として公開するか。
他言語比較
不変性の扱いは、言語によって大きく3つに分かれる。
- 可変が既定で、不変にするのは任意: 再代入も変更もできるのが通常で、
const/final/readonly/freezeなどで部分的に制限する(C++・Go・Java・C#・TypeScript・JavaScript・Python・PHP・Ruby)。 - 束縛は不変が基本で、可変は明示して選ぶ:
mut/var/refなどを付けたときだけ、再代入や変更ができる(Rust・Kotlin・Swift・Scala・OCaml・MoonBit・Zig)。 - データがすべて不変: 通常のデータは変更されず、更新は新しい値を作って表す(Haskell・Elixir)。
| 言語 | 再代入 | 値・オブジェクトの変更 | 不変にする手段 | 説明 |
|---|---|---|---|---|
| Rust | `let mut` で可能 | `mut` な束縛・`&mut` 参照で可能 | 束縛と参照の`mut`の有無 | 束縛は不変が基本で、`mut`を付けたときだけ再代入・変更できる。 束縛は既定で不変で、`mut`を明示したときだけ再代入と変更ができる。`&mut`は同時に1つしか存在できないため、共有と変更が同時に起きない。ただし変更は言語機能として存在し、`mut`・借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。・所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。を学ぶ必要がある。melangは変更そのものを設けないので、この仕組みを必要としない。 |
| MoonBit | `let mut`で可能 | `mut`フィールド・`Array`などで可能 | `let`、`mut`の有無、`immut`パッケージ | 不変と可変を言語上で区別し、可変にするときは`mut`で明示する。 不変な束縛・フィールドが既定で、変更するときは`mut`を付けて明示する。`Array`などは変更でき、不変なコレクションは`immut`パッケージとして別に用意される。不変と可変を言語上で区別する点はRustに近い。 |
| Swift | `var`で可能 | `var`なら可能 | `let`、値型(struct) | `let`と`var`を使い分ける。structは`let`にすると中身も固定される。 `struct`などの値型は代入時にコピーされるため、`let`にすれば中身も変更できず、共有による影響も出ない。一方で`class`は参照型で、`let`でもプロパティを変更できる。値型と参照型で不変の意味が変わる。 |
| Kotlin | `var`で可能 | 可能 | `val`(束縛のみ)、`data class`、読み取り専用コレクション | `val`は束縛を固定するが、参照先が不変とは限らない。 `val`と`var`を使い分け、`val`を基本とするのが慣習である。`List`は読み取り専用のインターフェースで、実体が`MutableList`の場合は別の参照から変更でき、不変が保証されるわけではない。`data class`の`copy`で、新しい値を作る書き方ができる。 |
| Java | 可能(`final`で禁止) | 可能 | `final`(束縛のみ)、`record`、不変コレクション | `final`は参照の再代入を防ぐだけで、参照先の変更は防がない。 `final`は束縛(変数・フィールド)を固定するだけで、参照先のオブジェクトは変更できる。`record`や`List.of`で不変にできるが、選ぶのは書き手である。不変コレクションの違反は、コンパイル時ではなく実行時に例外として現れる。 |
| Scala | `var`で可能 | 可能 | `val`、`case class`、不変コレクション(既定) | 既定のコレクションは不変。ただし`var`や可変コレクションも使える。 `val`・`case class`・不変コレクションが既定で、更新は`copy`や`:+`で新しい値を作って表す。一方で`var`と`scala.collection.mutable`も使えるため、不変は慣習であり、言語が強制するわけではない。 |
| C# | 可能 | 可能 | `readonly`、`record`、`init`、不変コレクション | 既定は変更可能。`readonly`・`record`・`init`で部分的に制限する。 `readonly`はフィールドの再代入を防ぐだけで、参照先は変更できる。`record`と`with`式で、新しい値を作る書き方が言語機能になっている。`ImmutableList`などの不変コレクションは別のライブラリで、使うかどうかは書き手が選ぶ。 |
| C++ | 可能 | 可能 | `const`(`const_cast`で外せる) | 既定は変更可能。`const`で制限できるが、回避する手段も残る。 既定は変更可能で、不変にするのは`const`を付ける側の任意である。`const`は参照やメンバ関数にも付けられ表現力が高いが、`const_cast`や`mutable`メンバで抜け道が残る。不変を保つには書き手の規律が要る。 |
| C | 可能 | 可能 | `const`(キャストで外せる) | 既定は変更可能。`const`はC++より歴史が古いが、キャストで容易に外せる。 `const`修飾子はC++より先にC89で導入されたが、既定は変更可能でありC++の`const`とほぼ同じ設計思想を持つ(C++はここに`const_cast`という専用の抜け道キャストを追加した)。Cでは通常のキャストで`const`を外せてしまい、文字列リテラルのように実際には読み取り専用領域に置かれるデータを書き換えようとすると、コンパイルは通るのに実行時に未定義動作になるという、コンパイル時の検査と実行時の実体が食い違う典型例を持つ。 |
| Go | 可能 | 可能 | `const`は定数のみ | struct・slice・mapは変更でき、不変にする仕組みはない。 `const`は数値・文字列・真偽値などのコンパイル時定数にしか使えず、struct・slice・mapを不変にする手段はない。文字列は不変だが、他のデータは変更できる。sliceは同じ配列を共有するため、別の変数から見た値が変わることがある。 |
| Zig | `var`で可能 | `var`な変数・非constポインタで可能 | `const`/`var`の有無、ポインタのconstness | `const`/`var`を明確に区別し、ポインタのconstnessも型に現れる。 `const`と`var`が束縛の可変性を明確に分け、`const`は再代入も中身の変更もできない。ポインタも`*T`(書き込み可)と`*const T`(読み取り専用)で型として区別され、`*const T`経由では書き込めない。Rustの借用所有権を移さずに、値を一時的に貸し出して使わせること。Rustでは`&T`(読み取り専用)と`&mut T`(変更可能)で表し、借りている間の書き換えや破棄をコンパイラが検査する。チェッカのような所有権値の持ち主を一つに定め、持ち主がいなくなったときに値を破棄する仕組み。Rustが代表例で、値を別の変数や関数へ渡すと持ち主が移り(move)、元の変数は使えなくなる。規則とは別物で、複数の可変ポインタが同時に存在することを言語が防ぐ仕組みではない点に注意。 |
| OCaml | 通常の`let`は再代入しない | `ref`・`mutable`フィールド・配列で可能 | 通常の`let`とレコード(既定) | 既定は不変だが、`ref`・`mutable`・配列で変更を明示的に使える。 `let`束縛とレコードは既定で不変で、`with`で新しい値を作れる。ただし`ref`・`mutable`フィールド・配列・`Bytes`など、変更を使う手段が言語に含まれる。関数型が中心の言語だが、変更を明示して選べる。 |
| F# | 再代入しない(`mutable`で可能) | レコードは既定で不変。`mutable`フィールドで可能 | `let`、レコード(既定)、`mutable`の有無 | 既定は不変で、変更するときは`mutable`を明示する。OCamlのref型も使える。 `let`束縛とレコードは既定で不変で、`with`で新しい値を作れる。変更したいフィールド・変数には`mutable`を明示的に付ける必要がある。OCaml由来の`ref`型(可変セル)も引き続き使え、.NETとの相互運用のためclassの可変プロパティも扱える。関数型が中心の言語だが、変更を明示して選べる点はOCamlと同じ。 |
| Haskell | 再代入しない | 純粋な値は不変 | 言語の既定(変更は`IORef`・`ST`など別の仕組み) | 束縛も値も不変。状態の変更は、別の仕組みとして分けて扱う。 束縛は再代入されず、データも不変である。更新は、レコード更新構文や関数で新しい値を作って表す。変更可能な状態は`IORef`・`ST`・`MVar`などとして型に現れ、純粋な処理と区別される。melangの方針に最も近いが、状態変更を型で区別する点は、melangでは決めていない。 |
| Erlang | 不可能(単一代入) | 通常のデータは不変 | 言語の既定(状態はプロセスが新しい状態へ遷移して表す) | 変数は一度束縛すると同じ値にしか再束縛できない、厳密な単一代入。 Elixirの`x = 1`に続く`x = 2`のようなrebindingすら許さず、同じ変数名への再束縛は既に束縛済みの値と一致する場合しか成立しない厳密な単一代入(single assignment)。データはすべて不変で、更新は新しい値を作って別の変数に束縛する。プロセスの状態は再帰で新しい状態に遷移させ、melangが目指す「値は不変・再代入は禁止」という設計に、束縛の面ではElixirより近い。 |
| Elixir | 再代入ではなくrebinding | 通常のデータは不変 | 言語の既定(状態はプロセスが新しい状態へ遷移して表す) | 変数は同じ名前へ束縛し直せるが、データは変更されない。 `x = 1`のあとに`x = 2`と書けるが、これは同じ名前を別の値へ束縛し直すだけで、元のデータは変わらない。データはすべて不変で、更新は新しい値を作って表す。プロセスの状態は、再帰で新しい状態に遷移させる。melangは、rebindingも再代入として禁止する点が異なる。 |
| TypeScript | `let`で可能 | 可能 | `const`(束縛のみ)、`readonly`(型のみ) | `const`は束縛だけ。`readonly`は型検査だけで、実行時には効かない。 `const`は束縛だけを固定する。`readonly`や`Readonly<T>`は型だけの制限で、実行時には何も防がず、readonlyを外した型へ代入できる場合もある。不変性は型検査の範囲に留まる。 |
| JavaScript | `let` / `var`で可能 | 可能 | `const`(束縛のみ)、`Object.freeze`(浅い) | `const`のオブジェクトも変更できる。`freeze`は浅く、任意の手段である。 `const`は束縛だけを固定する。`Object.freeze`は最上位のプロパティしか固定せず、入れ子のオブジェクトや配列の中身は変更できる。深く不変にするには、再帰的に凍結するか、ライブラリを使う必要がある。 |
| PHP | 可能 | 可能 | `const`・`readonly`プロパティ(8.1〜) | 変数もオブジェクトも既定は変更可能。`readonly`で個別に制限できる。 `readonly`プロパティ(8.1)と`readonly class`(8.2)で、初期化後の変更を禁止できる。配列は代入時に値として扱われる(コピーオンライト)ため、別の変数から見える影響は出ない。一方でオブジェクトは、ハンドルが共有される。不変にするかどうかは、書き手の選択である。 |
| Ruby | 可能 | 可能(破壊的メソッドも一般的) | `freeze`、`Data.define`(3.2〜) | オブジェクトは通常変更できる。`!`付きの破壊的メソッドも一般的。 文字列・配列・ハッシュなど、オブジェクトは通常変更でき、`sort!`や`<<`などの破壊的メソッドが一般的に使われる。`freeze`は浅く、定数への再代入も警告で済む。3.2の`Data.define`は、不変な値オブジェクトを作る手段である。 |
| Python | 可能 | 可能 | `tuple`・`frozenset`・`frozen=True`のdataclass | list・dict・オブジェクトは変更できる。不変な型は個別に用意されている。 `list`・`dict`・`set`・通常のクラスのインスタンスは変更でき、それが一般的な使い方である。`tuple`・`frozenset`・`str`・`frozen=True`のdataclassは不変だが、`frozen`は浅く、中身が`list`なら変更できる。定数を宣言する仕組みは言語にない。 |
比較の観点は次の通りである。
- 変数への再代入を許すか。
- 束縛を固定したときに、参照先の値も不変になるか。
- コレクションの破壊的変更を許すか。不変なコレクションが標準か。
- 不変にする単位は何か(束縛・フィールド・型・コレクション)。
- 可変と不変の使い分けを、書き手に選ばせるか。
- 状態を持つ処理を、言語がどう表すか。
Java・Kotlin・TypeScript・JavaScriptは、final / val / constで束縛を固定できるが、参照先は不変にならない。KotlinのListは読み取り専用のインターフェースで、実体がMutableListなら、別の参照から変更される。TypeScriptのreadonlyは型検査だけの制限で、実行時には何も防がない。JavaScriptのObject.freezeは浅く、入れ子の中身は変更できる。C++のconstも、const_castで外せる。
Rust・Swift・Scala・MoonBit・Zigは、不変と可変を言語上で区別する。Rustは、mutの有無に加えて、変更可能な参照を同時に1つに限ることで、共有と変更が同時に起きないようにする。Swiftは、structのような値型をletにすると中身も固定されるが、参照型のclassはletでも中身を変更できる。Scalaは、不変なコレクションが既定だが、varと可変コレクションも使えるため、不変は慣習にとどまる。Zigはconst/varで束縛の可変性を分け、ポインタも*T/*const Tで書き込み可否が型に現れるが、Rustのように借用の同時性を制限する所有権規則は持たない。
Python・PHP・Rubyは、既定でオブジェクトを変更でき、sort!のような破壊的メソッドも一般的である。不変にするには、frozen=Trueのdataclass・readonlyプロパティ・freeze・Data.defineなど、言語ごとの手段を選ぶ。
HaskellとElixirは、データがすべて不変である。Haskellは、束縛も値も不変で、変更が必要な状態はIORef・STなどの別の仕組みとして、型に現れる。Elixirは、x = 1のあとにx = 2と書けるが、これは同じ名前を別の値へ束縛し直す(rebinding)だけで、元のデータは変わらない。プロセスの状態は、再帰で新しい状態へ遷移させて表す。OCamlも、通常のlet束縛とレコードは不変だが、ref・mutableフィールド・配列で変更を明示的に使える。
melangは、Haskell・Elixirに近く、通常のデータを常に不変とし、更新を新しい値の生成として表す。ただし、Elixirのrebindingも再代入として禁止する点と、変更を選ぶ逃げ道を設けない点が異なる。
トレードオフ
可変が既定で、不変は任意の設計(Java, C#, Python, JavaScript, Rubyなど)
- 利点: ループ内の累積や、コレクションへの追加を、素直に書ける。慣れた書き方が使え、既存のコードやライブラリとも合わせやすい。
- 欠点: 共有された値を、意図せず書き換えてしまう。防御的コピーが必要になる。
final・constは束縛だけで、参照先の不変は別に管理する必要がある。
不変が基本で、可変を明示して選ぶ設計(Rust, Kotlin, Swift, Scala, Zigなど)
- 利点: 既定では不変なので、変更は
mut/varという印で読み取れる。必要な箇所では、変更を使って効率よく書ける。 - 欠点: 可変と不変の使い分けを、書き手が判断し続けることになる。束縛が不変でも、参照先が不変とは限らない言語がある。Rustは、
&mutや借用のルールを学ぶ必要がある。
データがすべて不変な設計(Haskell, Elixirなど)
- 利点: 値の共有で、意図しない書き換えやデータ競合が起きにくい。状態の変化が、値から値への変換として明示される。
- 欠点: 累積や、コレクションの更新が冗長になり得る。新しい値を作るコストが避けられず、永続データ構造や最適化が必要になる。状態を持つ処理を、別の仕組みで表す必要がある。
melangの選択
melangは、Haskell・Elixirと同じく、通常のデータをすべて不変とし、再代入・フィールド変更・コレクションの破壊的変更を禁止する。手続き的な処理は禁止しない。
- Pros: aliasを共有しても、データ競合や意図しない書き換えが起きにくい。「関数の引数」で決めた「常に共有して渡す」設計と整合する。状態の変更が、値から値への変換として明示される。防御的コピーや、可変と不変の使い分けを、利用者に要求しない。
- Cons: ループ内の累積(
sum += xなど)が書けなくなる。コレクションの更新や、状態を伴うアルゴリズムが冗長になり得る。永続データ構造やコンパイラの最適化など、実装上のコストと戦略が必要になる。
書き換えられる自由よりも、値が変わらないという保証を優先する。