← melang

値の不変性

Last Updated: 2026-09-28 02:00

結論

melangでは、すべてのデータを不変(イミュータブル)とする。不変にするかどうかを、書き手に選ばせない。

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が不変にするのはデータであり、if・for・early return・I/Oのような手続き的な処理は禁止しない。禁止しないと、手続き的な書き方が使える。

不変性と手続き的な処理は別の軸である。問題の原因は、共有された値が書き換わることであり、処理を順に書くことではない。手続き的な処理を残すことで、書き手が慣れた制御構文を使い続けられる。

このnoteで決めないこと

他言語比較

不変性の扱いは、言語によって大きく3つに分かれる。

言語再代入値・オブジェクトの変更不変にする手段説明
Rust`let mut` で可能`mut` な束縛・`&mut` 参照で可能束縛と参照の`mut`の有無
MoonBit`let mut`で可能`mut`フィールド・`Array`などで可能`let`、`mut`の有無、`immut`パッケージ
Swift`var`で可能`var`なら可能`let`、値型(struct)
Kotlin`var`で可能可能`val`(束縛のみ)、`data class`、読み取り専用コレクション
Java可能(`final`で禁止)可能`final`(束縛のみ)、`record`、不変コレクション
Scala`var`で可能可能`val`、`case class`、不変コレクション(既定)
C#可能可能`readonly`、`record`、`init`、不変コレクション
C++可能可能`const`(`const_cast`で外せる)
C可能可能`const`(キャストで外せる)
Go可能可能`const`は定数のみ
Zig`var`で可能`var`な変数・非constポインタで可能`const`/`var`の有無、ポインタのconstness
OCaml通常の`let`は再代入しない`ref`・`mutable`フィールド・配列で可能通常の`let`とレコード(既定)
F#再代入しない(`mutable`で可能)レコードは既定で不変。`mutable`フィールドで可能`let`、レコード(既定)、`mutable`の有無
Haskell再代入しない純粋な値は不変言語の既定(変更は`IORef`・`ST`など別の仕組み)
Erlang不可能(単一代入)通常のデータは不変言語の既定(状態はプロセスが新しい状態へ遷移して表す)
Elixir再代入ではなくrebinding通常のデータは不変言語の既定(状態はプロセスが新しい状態へ遷移して表す)
TypeScript`let`で可能可能`const`(束縛のみ)、`readonly`(型のみ)
JavaScript`let` / `var`で可能可能`const`(束縛のみ)、`Object.freeze`(浅い)
PHP可能可能`const`・`readonly`プロパティ(8.1〜)
Ruby可能可能(破壊的メソッドも一般的)`freeze`、`Data.define`(3.2〜)
Python可能可能`tuple`・`frozenset`・`frozen=True`のdataclass

比較の観点は次の通りである。

  1. 変数への再代入を許すか。
  2. 束縛を固定したときに、参照先の値も不変になるか。
  3. コレクションの破壊的変更を許すか。不変なコレクションが標準か。
  4. 不変にする単位は何か(束縛・フィールド・型・コレクション)。
  5. 可変と不変の使い分けを、書き手に選ばせるか。
  6. 状態を持つ処理を、言語がどう表すか。

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など)

不変が基本で、可変を明示して選ぶ設計(Rust, Kotlin, Swift, Scala, Zigなど)

データがすべて不変な設計(Haskell, Elixirなど)

melangの選択

melangは、Haskell・Elixirと同じく、通常のデータをすべて不変とし、再代入・フィールド変更・コレクションの破壊的変更を禁止する。手続き的な処理は禁止しない。

書き換えられる自由よりも、値が変わらないという保証を優先する。