Kotlinでのイミュータブルなコードの書き方

## はじめに

先日、「イミュータブルの嬉しさって何だっけ」という記事で、イミュータブルなコードのメリットを整理しました。

https://qiita.com/fujithuro/items/aed133583e28639427f0

その記事では具体的なコードの実現方法には触れなかったので、それをこちらの記事で改めてまとめようと思います。

ちなみにコードはKotlinを使っていますが、考え方などは他の言語にも通用する部分は多いかと思います。

## イミュータブルの実現方法

**イミュータブル**（`immutable`）とは「不変（変更できない）」ことを指す言葉です。
「変更できない」にも色んなパターンがあるので、それらの実現方法を1つ1つ説明していきます。


### 参照をイミュータブルにする: `val`と`var`

Kotlinでは、変数宣言で使えるキーワードとして`val`と`var`があります。

* val: 再代入不可（イミュータブルな参照）
* var: 再代入可（ミュータブルな参照）

```kotlin
val num: Int = 20
num = 40  // コンパイルエラー

var moji: String = "Hello!"
moji = "GoodBye!"  // ok
```

#### 読み取り専用とイミュータブル

ただし、`val`は**オブジェクトの中身をイミュータブルにするものではない**ことに注意が必要です。

```kotlin
val sample = Sample()
sample.num = 100  // オブジェクトのプロパティはvarなので変更できる

class Sample {
    var num: Int = 20
    var moji: String = "Hello"
}
```

```kotlin
val list = mutableListOf(1, 2, 3)
list.add(4)  // コレクションに追加できる
```

このように、`val`だけだと参照を変えられない（再代入できない）だけで、中身は変えられます。ちなみにこのような状態は`read-only`（読み取り専用）と表現され、`immutable`とは区別されます。

中身まで含めて不変にしたければ、この後に説明する「イミュータブルなオブジェクト」「イミュータブルなコレクション」の考えも必要です。


## イミュータブルなオブジェクト

オブジェクトをイミュータブルにする場合、以下の条件を満たす必要があります。

* 全てのプロパティがval
* プロパティの型自体がイミュータブルであること
    * `Int`, `Boolean`などプリミティブな値を表す型[^primitive]
    * String
    * Listなど、後述するイミュータブルなコレクション
    * など
* 継承などでプロパティの可変性を変更できないこと（e.g. finalなクラス）
    * ちなみにKotlinのクラスはデフォルトで`final`

[^primitive]: Kotlinでは、`Int`や`Boolean`はプリミティブ型でなくオブジェクト型です。そのため「プリミティブな値を表す型」と少し濁した書き方をしていますが、JVM上ではプリミティブ型に最適化されるため、プリミティブのような型として理解して頂いても概ね問題ありません。ただ、言語仕様としては`value type`（値型）と表現することが正確であることを申し添えておきます。


### data class

Kotlinの`data class`は、イミュータブルなクラスを扱うのに向いています。

```kotlin
data class User (
    val id: Long,
    val name: String,
)
```

この`User`クラスは「すべてのプロパティがval」であるなど、前述の条件を満たしています。そのうえで、`data class`に標準で用意されている`copy()`関数を使えばインスタンスの複製も安全かつ簡潔に行えます。

```kotlin
val user = User(1001, "Alice")
val renamedUser = user.copy(name = "Bob")
```

これにより、「元のインスタンスを変える」のではなく「新しいインスタンスを生成する」設計を自然に表現できます。


## イミュータブルなコレクション

Kotlinはコレクションにもイミュータブルとミュータブルがあります。
例として、ここでは`List`を使って説明しますが、`Map`や`Set`など他のコレクションも基本的な考え方は同じです。

```kotlin
val list: List<Int> = listOf(1, 2, 3)
list.add(4)  // コンパイルエラー。Listという型に`add`という関数は無い

val list: MutableList<Int> = mutableListOf(1, 2, 3)
list.add(4)  // ok
```

型名に`Mutable`と付いている方（`MutableList`）がミュータブルなコレクションで、こちらにしか要素を変更する関数（`add`など）はありません。

なお`Mutable`が付いていない方（`List`）は完全にイミュータブルな訳ではなく、あくまで読み取り専用のアクセスのみが提供されている型です。


### 型のイミュータビリティと中身のイミュータビリティ

少しややこしいポイントとして、型がイミュータブルかどうかだけではなく、中身のオブジェクトが実際にイミュータブルかも重要です。

例えば下記のような関数があったとします。

```kotlin
fun getList(): List<String> {
    val list = mutableListOf("a", "b")
    return list  // 戻り値の型はListでも中身はMutableList
}
```

`MutableList`は`List`を継承しているので、このような関数も問題なく定義できます。

で、何がややこしいかというと、この関数が返すインスタンスはミュータブルなので、ダウンキャストされるなどしてミュータブルな型として扱われると、戻り値がイミュータブルな型（`List`）だったのに変更される可能性がある、という点です。

```kotlin
val list = getList()

if (list is MutableList) {
    list.add("c")  // 変更できてしまう
}
```

もちろん、こういったダウンキャストは推奨されない書き方なので、ちょっと無理矢理な印象があるかもしれませんが、JavaとKotlinのコードが混在している場合、Javaでよく使われる`ArrayList`などはミュータブルなので、こういった問題が起こるリスクは現実的なレベルであり得ます。

### 対処法

`toList`という関数を呼び出すことで対処できます。

```kotlin
fun getList(): List<String> {
    val list = mutableListOf("a", "b")
    return list.toList()  // 読み取り専用の新しいリストが返される
}
```

これにより、関数が返すインスタンスも読み取り専用になるため、前述のような問題を起こさなくなります。


## おわりに

[前回の記事](https://qiita.com/fujithuro/items/aed133583e28639427f0)では触れなかった、イミュータブルなコードの具体的な実現方法について整理してみました。

特にコレクションに関しては少々ややこしい部分もあるので、実際に書いて動かしながら挙動を把握するのが良いと思います。