[Kotlin] なぜMutableListとListが分かれているのか

# はじめに

Kotlinでは`add`や`remove`などの変更ができる`MutableList`と、変更ができない`List`が分かれています。なぜこのように分ける必要があるのでしょうか？

このようなタイトルですが、本記事は`MutableList`と`List`の話に限定されません。
Kotlin（などの参照型中心の言語）には`MutableXXX`: ミュータブルクラスと`XXX`: イミュータブルクラスの二つのクラス（インターフェース）に分かれて設計されているケースが多くあります。なぜこれらを分ける必要があるのでしょうか？

# valとvar

「`var`は変数だから変更ができて、`val`は定数だから変更できない」のように簡潔に説明されることが多いかなと思います。

```kotlin
var a = 0
a += 1

val b = 0
// b += 1 コンパイラエラー
```

この話には続きがあります。

# valは「参照」を定数にするだけ

valが定数（変更不可）にするのは
**クラスの「参照」だけであって、「実態」=クラスの中身は変更できます。**

```kotlin
class Counter {
    private var count = 0

    fun getCount() = count

    fun increment() {
        count += 1
    }
}

fun main() {
    val counter = Counter()

    println(counter.getCount()) // 0
    
    counter.increment()
    println(counter.getCount()) // 1
}
```

できないのは「参照」への再代入であって、「実態」の変更ではありません。

```kotlin
fun main() {
    val counter = Counter()
    // counter = Counter() これができないだけ
}
```

なぜなら、`class`が参照型だからです。

`val counter = Counter()`と書いた時、コンピュータ上のメモリでは以下のようになっています。

```mermaid
classDiagram
    class counter {
        0x1234...
    }

    class `0x1234...` {
        count: 0
    }
```

counter変数に`0x1234...`という「実態」の場所を示す「参照≒メモリアドレス」が保持されており、アドレス`0x1234...`に`Counter`クラスの実態が存在します。
（単なる変数ならIntはJavaのプリミティブ型（値型）の`int`になるので、`0x1234...`に直接`counter: 0`の値が存在する）

この時、`val`によって定数になるのは`counter` が持つ`0x1234...`の値それ自体だけなので、アドレス`0x1234...`に存在する`count`は変更できるわけです。

一方で、`counter` が持つアドレス`0x1234...`を別のアドレスに変えることはできない（再代入はできない）というわけです。

# 実態も定数にしたい

ここで、「実態も変更不可にしたい」という需要が生まれます。
例えば、他の関数やクラスに読み取り専用で渡したり公開する場合です。

変更可能なインスタンスの参照を闇雲に公開すると、意図せず変更が伝播してバグの温床になるからです。

```kotlin
fun main() {
    val counter = Counter()
    counter.increment()
    
    show(counter)
}

fun show(counter: Counter) {
    // ここでは読み取るだけにしたい    
    println(counter.getCount())
    
    counter.increment() // これを禁止したい
}
```

```kotlin
class HogeViewModel {
    val counter = Counter()
}

class HogeView {

    val viewModel = ViewModel()

    fun doSomething() {
        // ここでは読み取るだけにしたい    
        println(viewModel.counter.getCount())
        
        viewModel.counter.increment() // これを禁止したい
    }
}
```

その解決策の一つが、**イミュータブルクラス** です。

内部の状態が変更できる（varになっており、操作メソッドがある）`Mutable`版`Counter`と
内部の状態が全て変更できない（valになっており、操作メソッドがない）`Immutable`版`Counter`を作ります。

```kotlin
class MutableCounter {
    private var count: Int = 0

    fun getCount() = count

    fun increment() {
        count += 1
    }

    fun asImmutable() = ImmutableCounter(count)
}

class ImmutableCounter(val count: Int) {}
```

これを用いて先ほどの処理を書き直します。

```kotlin
fun main() {
    val counter = MutableCounter()
    counter.increment()
    
    show(counter.asImmutable())
}

fun show(counter: ImmutableCounter) { 
    // counter.increment() 禁止できた
}
```

```kotlin
class HogeViewModel {
    private val _counter: MutableCounter = MutableCounter()
    val counter: ImmutableCounter get() = _counter.asImmutable()
}

class HogeView {

    val viewModel = ViewModel()

    fun doSomething() {
        // viewModel.counter.increment() 禁止できた
    }
}
```

このように、変更したい場所では`MutableCounter`を利用し、それを`ImmutableCounter`に変換して読み取り専用として公開することができます。

つまり
**なぜMutableListとListが分かれているのか** 
の理由（の一つ）は
**valやvarでは制限しきれない実態の変更を制限し、意図しない値の変更の伝搬を防ぐため** 
になります


基本的なKotlin/Androidのクラスには多くのミュータブル・イミュータブルクラスのペアが存在します。

- `MutableList`, `List`
- `MutableSet`, `Set`
- `MutableLiveData`, `LiveData`
- `MutableStateFlow`, `StateFlow`

お気づきかもですが、基本的に`Immutable`は省略します。
なぜなら、Kotlin/Androidで提供されているクラスは基本的にImmutableで実装されていることが多いからです。（基本が`Immutable`で、例外的に`Mutable`が存在するというイメージ）

# 実際にはインターフェースを用いて設計される


実際の`MutableList`, `List`などは、`interface`を用いて設計されていることが多く
`MutableCounter: ImmutableCounter`という子関係になっていることが多いです。

https://github.com/JetBrains/kotlin/blob/0938b46726b9c6938df309098316ce741815bb55/core/builtins/native/kotlin/Collections.kt#L179

```kotlin
// インターフェース
interface ImmutableCounter {
    fun getCount(): Int
}

interface MutableCounter: ImmutableCounter {
    fun increment()

    fun asImmutable(): ImmutableCounter
}

// 実装
class ImmutableCounterImpl(val count: Int): ImmutableCounter {
    override fun getCount() = count
}

class MutableCounterImpl: MutableCounter {
    private var count: Int = 0

    override fun getCount() = count

    override fun increment() {
        count += 1
    }

    override fun asImmutable() = ImmutableCounterImpl(count)
}
```

実は`asImmutable()`を呼んでクラスを変換しなくとも、そのまま`MutableCounter`の変数を`ImmutableCounter`として受け取って利用できます。

```kotlin
fun main() {
    val counter = MutableCounterImpl()
    counter.increment()
    
    show(counter)
}

fun show(counter: ImmutableCounter) { 
    println(counter.getCount())
    // counter.increment() 禁止できた
}
```

実際に`MutableList`で試した例

```kotlin
fun main() {    
    val mutable = mutableListOf(1)
    sub(mutable)
}

fun sub(list: List<Int>) {
    println(list.last()!!)
}
```

:::note info
補足

しかし、クラスを変換しないと
キャストによって`MutableList`に戻すことができてしまうため注意が必要でしょう。

```kotlin
fun main() {    
    val mutable = mutableListOf(1)
    sub(mutable)
}

fun sub(list: List<Int>) {
    println(list.last()!!)
    
    val mutable = list as MutableList<Int>
    mutable.add(2)
    println(list.last()!!)
}
```
:::


# イミュータブルクラスの他の視点

イミュータブルクラスには他の視点があります。簡単に紹介します。

## イミュータブルクラスのみにし、再生成によって値を変更する

値を変更したい需要がある場合、イミュータブルクラスのみで済ませ、インスタンス自体を再生成することによって値の変更を強制する設計があります。例えば`String`です。

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-string/

Stringには「自分自身を変更するメソッド **（破壊的メソッド）** 」は一つも実装されていません。
その代わりに「自分自身に変更を加えた新しいStringインスタンスを返すメソッド **（非破壊的メソッド）** 」は実装されているので、常に新しいStringインスタンスを再生成することで変更を行います。

```kotlin
fun main() {
    var hello = "Hello"

    // "Hello World"のStringを作成し、helloに再代入している
    hello += " World"

    val hello2 = "Hello"
    // hello2 += " World" コンパイルエラー
}
```

これによって、ミュータブル・イミュータブルクラスの変換よりも単純に変更を制限することができます。

ミュータブルクラスと比べたデメリットとして

- インスタンス全体を再生成するのでコストが高い
- 再生成、再代入の処理を書くのがめんどくさい

というのがあります。
そのため、複雑な構造を持ちコストが高い`List`や`Set`や`StateFlow`などのデータホルダーではミュータブルクラスが選択されやすく、単なる一つのデータである`String`などではミュータブルクラスのみが選択されやすいと考えられます。

### data class

https://kotlinlang.org/docs/data-classes.html

実は`data class`はこの考え方に基づいた型です。
（`data class`のプロパティは`var`で宣言できるのでミュータブルにも一応できますが）

型を`data class`で定義すると`copy`メソッドが自動的に作成されます。
この`copy`メソッドを用いることで、非破壊的に値を更新できます。

```kotlin
data class Message(val id: Int, val content: String)

fun main() {
    val hello = Message(1, "Hello")
    // hello.content += " World" コンパイルエラー
    
    val helloWorld = hello.copy(content = hello.content + " World")
}
```

ただ、`copy`メソッドがあってもやはりめんどくさい場合があります。
それはネストされたデータ構造を変更する時です。

```kotlin
data class Lecture(val id: Int, val name: String, val subject: Subject)
data class Subject(val id: Int, val name: String, val color: String)

fun main() {
    var lecture = ...
    lecture = lecture.copy(subject = message.subject.copy(color = "#F6F6F6"))
}
```

:::note info
TypeScriptのスプレッド構文

TypeScriptを書いたことのある方は、`data class`の説明でピンと来た方もいらっしゃると思います。
そうです。TypeScriptのスプレット構文は`data class`のcopyと同じ考え方です。

```ts
let lecture = ...
lecture = { 
    ...lecture,
    subject: {
        ...lecture.subject,
        color: "#F6F6F6"
    } 
}
```
:::


## 実態の変更を検知しにくい

ミュータブルクラスには他の問題もあります。
クラスの実態が変更されても、実態が変更されたことが参照側から検知しにくい点です。


例えば`MutableList`に要素が追加されても、`mutableList`変数が指すアドレス（参照）には変化がないので、値の変更が起きたことがわかりません。

```kotlin
val mutableList = mutableListOf(1)
mutableList.add(2)
```

```mermaid
classDiagram
    class mutableList {
        0x1234...
    }

    class `0x1234...` {
        values: ...
    }
```

これがどういう問題になるのかというと、JetPackComposeで問題になっています。

ToDoリストを作成するColabにてこんな記述があります。

https://developer.android.com/codelabs/jetpack-compose-state?index=..%2F..index&hl=ja#10

> 次に、リストからタスクを削除する動作を追加するには、まずリストを可変リストにします。
> このために可変オブジェクト（ArrayList<T> や mutableListOf, など）を使用しても機能しません。これらの型は、リスト内のアイテムが変更されたことを Compose に通知せず、UI の再コンポーズをスケジュール設定しません。別の API が必要です。
> Compose で監視できる MutableList のインスタンスを作成する必要があります。この構造により、Compose は変更を追跡して、リストでアイテムを追加または削除したときに UI を再コンポーズできます。

JetPackComposeの`LazyColumn`で`MutableState<MutableList<T>>`を用いた場合、`MutableList`から要素を削除・追加しても、変更を検知できず、再コンポーズできません（画面に変更が反映されません）。
なぜなら、`MutableState`は参照が変化しないと変更を検知しないからです。

https://developer.android.com/reference/kotlin/androidx/compose/runtime/MutableState

そのため、要素の削除・追加を検知できるように実装した専用のListである`SnapshotStateList`を用いる必要があります。（実際には`toMutableStateList`等を使います）

https://developer.android.com/reference/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList

さらに
これだけでは要素の追加・削除は検知できても、要素の値の変更までは検知できないため、値の変更をする場合は

- 値の変更をしたいプロパティを`MutableState`型にする
- Listからの削除し、値を変更した要素を追加する（実質的には再代入）

のどちらかをしなければいけないと記述があります。

> Compose が MutableList について追跡しているのは、要素の追加と削除に関連する変更であるためです。これが削除の仕組みです。ただし、行アイテムの値（この場合は checkedState）の変更は、それについても追跡するよう指示しない限り、認識されません。
> この問題は、次の 2 つの方法によって解決できます。
> ・checkedState が Boolean ではなく MutableState<Boolean> になるように、データクラス WellnessTask を変更します。これにより、Compose はアイテムの変更を追跡できます。
> ・変更しようとしているアイテムをコピーし、リストからそのアイテムを削除して、変更後のアイテムを再度リストに追加します。これにより、Compose はそのリストの変更を追跡します。

これらの煩雑さは、参照型の根本的な仕組みが原因で起きている問題です。

# 最後に

本記事は[Heart of Swift: Value Semantics を持たない型の問題と対処法](https://heart-of-swift.github.io/value-semantics/problems-without-value-semantics) を大幅に参考にしています。このサイトでは、本記事で触れた内容をSwiftがどうやって解決しているのかを解説しています。

Swiftは値型中心の言語です。Swiftはミュータブルクラス・イミュータブルクラスを用いず、値型を用いることで今回の問題をシンプルに解決しています。また、値型を中心に設計されたSwiftUIは、`State<Array<T>>`だけで配列の要素の削除・追加・変更まで監視可能になっています。

興味のある方は、ぜひ[Heart of Swift](https://heart-of-swift.github.io)にアクセスしてみてください。