はじめに
日頃、Kotlinでコーディングしている際に、「変数の宣言にはvar
ではなくval
を使う」「ミュータブルではなくイミュータブルなコレクションを使う」などはいつも意識してきました。
ただ、ある日ふと「何でそうした方がいいんだったっけ?」と湧いた疑問に即答できなかったので、おさらいとして記事にまとめます。
ちなみに本記事のサンプルコードではKotlinを使用しますが、考え方などは他の言語にも通ずるものかと思います。
この記事ではプログラムの変数やデータ構造の不変性についてのみ触れ、データベースやログの不変性などには触れません。
イミュータブルの基礎
まず、そもそもの前提ですがイミュータブル(immutable
)とは「不変(変更できない)」ことを指す言葉です。この「変更できない」ことによる恩恵を本記事では解説します。
なお、具体的にどうやってコードをイミュータブルにするのか、という観点はこの記事では割愛し、別の記事で解説する予定です。
嬉しさ
1. 動作が安定し、コードが読みやすい
これが、イミュータブルがもたらしてくれる恩恵のメインです。
変数の値やオブジェクトの状態が変わらない(常に同じ)ので、いつどのタイミングでも同じ値が取得できます。
「当たり前じゃん、それの何が嬉しいの?」と言われそうなので、ミュータブルな(変更可能な)場合と対比しながらもう少し詳しく説明します。
値が常に変わらないため、認知負荷が小さい
ミュータブルな場合、変数やオブジェクトの状態がどこかのタイミングで変わっている可能性があります。例えば、特定の条件で(特定のif分岐を通ると)少し値が変わる、などですね。
こうなると、「この時点では、このデータは何になってるんだろう?」と常に気を配りながらコードを読む必要があり、認知負荷が大きくなります。
簡単に言うとコードを読むのがしんどいです。
ミュータブルなコードのつらさが極まっている例として「グローバル変数」があります。グローバル変数はいつでも誰でも変更可能な変数です。そのため、いつ、何のきっかけで、どんな値に変更されたのか把握するのがとても大変です。
グローバル変数が絡んでいる処理で不具合が発生した場合、その原因調査も困難を極めます。もう経験したくない思い出です。
反面、イミュータブルなコードでは変数やオブジェクトの状態が生成された時点から変わりません。変更を加える場合、元のオブジェクトを変更するのではなく、異なる値を持つ別オブジェクトを新しく生成します。
val items: List<Item> = listOf(...)
// 元のitemsは変更されず、新しい価格を持つリストが生成される
val discountedItems = items.map { it.copy(price = it.price * 0.5) }
コードの特定の箇所でオブジェクトがどんな状態なのかは明白なため、コードの予測可能性や可読性が高まり、デバッグも容易になります。
スレッドセーフティの向上
イミュータブルな設計は、マルチスレッド環境でも有利に働きます。
他のスレッドによって変更されていることを心配しなくていい(データの一貫性が保証されている)ため、複雑なロック処理などを設ける必要がありません。
簡潔で安定した並行処理を実現しやすくなります。
2. 関数型プログラミング原則との整合性
関数型プログラミングにおいて、イミュータビリティは核心的な概念の一つです。
前述した「スレッドセーフティ」に加えて、「純粋関数の実現」にも深く関連しています。
純粋関数と宣言型プログラミング
純粋関数
とは、下記の条件を満たす関数のことを言います。
- 同じ入力に対して常に同じ出力を返す
- 副作用を持たない、つまり(意図せず)外部の状態を変更することがない
純粋関数とイミュータブルデータを組み合わせると、常に安定した挙動が得られるので複雑さが大幅に削減されますし、状態変更できないことで副作用も排除しやすくなります。
関数型プログラミングで純粋関数が重視されているのは、振る舞いを予想しやすいシンプルな部品を作成することで、より宣言型プログラミングのアプローチを実現しやすくなるためです。言い換えれば「どのように(How)達成するか」ではなく「何を(What)達成するか」に注力できるため、コードの意図が明確になります。
val numbers = listOf(1, 2, 3, 4, 5, 6)
val result = numbers
.filter { it % 2 == 0 } // 「偶数である」という条件で絞る
.map { it * 10 } // 「10倍する」という変換を施す
.take(2) // 「最初の2要素を取る」
このコードには「どのように」フィルタリングや変換を行うかは登場せず、あくまで「何を達成したいか」だけを羅列した宣言的なコードになっています。
このため意味を理解しやすいですし、将来的な変更が必要となった場合の保守も容易です。
注意点
では、常にイミュータブルな設計が最善なのかと言えば、必ずしもそうではありません。
あえて(部分的に)ミュータブルな設計を取り入れることが合理的な場合もあります。その代表例はパフォーマンスが問題になるケースです。
例えば、あるオブジェクトのプロパティが頻繁に変更されるようなケースを考えてみます。
変更される度にオブジェクトを再生成するのでは、メモリの使用量などが増大してパフォーマンスを悪化させる懸念があります。そのようなケースでは、あえて再代入可能なプロパティとして定義し、イミュータブルな設計を(部分的に)崩すことにも一考の余地があります。
ただ、その場合もプロパティをpublicに公開すると無秩序に変更される恐れがあるため、アクセサ経由でしか参照や変更ができないようなカプセル化は意識するべきでしょう。
この他にも、たとえば数百万件規模の大規模なデータコレクションに対する操作などは、ミュータブルに扱おうとするとパフォーマンスを悪化させる懸念があります。
おわりに
即答できなかった「イミュータブルって何が嬉しいんだっけ」という問いについて整理してみました。
基本的なことですが、とても大事なことなので、忘れたり思い出したりするのではなく、当たり前に自分の中に在るようにしたいですね。