こんにちは、いからしです。

今日はイミュータブルプログラミングについて書いていきます。

対象読者は

  • コードレビューで先輩から「君ここイミュータブルじゃないよね?」と指摘を受けて「はい、そうですね(…いみゅーたぶる?)」状態の若手エンジニア
  • コードはかけるようになってきたから、ここらでちょっとモダンな書き方身につけて同期にマウントとったろ!!」な若手エンジニア
  • イミュータブルってお前w オブジェクト更新するたびに新しいオブジェクト作るやつだろww 非効率www」と思っている若手エン(ry

あたりです、よろしくお願いします。

 

イミュータブルってなーに?

まずはみんな大好き、Wikipediaさんの冒頭を引用。

イミュータブル (英: immutable) なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのことである。対義語はミュータブル (英: mutable) なオブジェクトで、作成後も状態を変えることができる。

Wikipediaさんにしてはわかりやすい説明。

これ以上の説明はいらない感もありますが、もう少しわかりやすくなるように一応コードも書いてみます。今回はRubyで。

Rubyの配列(Array)はミュータブルなので、pushメソッドで値を追加したりpopメソッドで末尾の値を取り除いたりと、オブジェクトの状態を変更することができます。しかしfreezeというインスタンスメソッドをコールすると、そのオブジェクトはイミュータブル状態となり、それ以降はpushやpopをコールするとFrozenErrorというエラーが発生するようになります。

ちなみに言語によってマチマチですが、後からイミュータブルにするのではなく最初からイミュータブルなオブジェクトというのも存在します。例えばRubyでいうと整数(Integer)や小数(Float)などの各種数値型クラスや、シンボル(Symbol、イミュータブルな文字列のようなもの)などがデフォルトでイミュータブルです。

少し脱線しますが、Rubyで扱えるほぼ全てのクラスの親クラスであるObjectクラスは、frozen?というインスタンスメソッドを持っています。このメソッドはそのインスタンスがイミュータブルであるか否かを真偽値で返すため、色々なオブジェクトがデフォルトでイミュータブルかどうかを確認するのに便利です。素敵ですねRuby(圧)。

 

ふーん。で、何が嬉しいの?

イミュータブルな状態のオブジェクトでプログラミングするメリットはざっくり以下の2つです。

  • 意図しない不具合を抑制できる
  • プログラムの可読性、保守性が向上する

状態が変わらないことが保証されているため、「え?なんでなの?なんでここでそんな出力になるの?ねえなんで…?」とか「やばい、このHashMapいろんなところで使い回されてて迂闊に中身を削除できねぇ…」とか「定数定義してたアクセス許可IP配列をなぜかpopしてる処理があって、特定のロジックを通るたびにアクセスできるIPが減る…(実話)」など、その手の問題が発生しなくなります。

と文字で説明されただけではピンとこないかもしれないので例を一つ。

上のコードが実行された後では、arrがどのような状態であるかわかりづらく、この後でarrを使った処理を実装するためにはarrの状態に応じた処理を行う条件分岐やポリモフィズムが必要となってしまいます。また最初はうまく動いていたとしても、func1の実装を後から変更して、引数として渡されたarrの状態を変更してしまうと、それ以降の処理が意図した通りに動かなくなるかもしれません。そんな破壊的なコードを実装するプログラマには厳しい制裁を科すのが効率的な(ry

ではイミュータブルなオブジェクトとして書くとどうなるでしょうか。

まずfunc1で状態が変更されるかもしれない問題は、arrをイミュータブルにしたことでFronzenErrorが発生するようになるため直ぐに気づけるようになりました。また条件分岐によるarrの加工処理ですが、こちらもarr自体を変更できないため新しい変数を宣言して、そこにarrの値を元にした新しい配列が代入されるような処理を書くことになります。このように記述することで7行目以降の処理が「arrの値を元に新しい配列を作る処理」に限定され、ミュータブル版の「hoge = ‘aaa’」のような関係ない実装が一緒に行われる可能性が低くなります(もちろん絶対ではないですが…)。

 

なるほどね。でもさ、それ状態変えたくなるたびに新しいオブジェクト作るでしょ?めっちゃメモリ食うんじゃない?

OK。あなたの言いたいことはつまりこういうことですね?

要素数3つのarrに1つ要素を追加したnew_arrを作ると、元のarrの要素3つ分に加えてnew_arrの要素4つ分、合計7つ分のメモリを食うのだろうと。これが要素の追加や削除のたびに発生して大変なことになるのだろうと。

ええ、わかりますよ。私にもそう思っていた時期がありました。でもね、実際はこうなんです。

新しい配列が抱える要素のうち、arrと同じものについてはオリジナルのarrを参照するだけで済むのです。arr自体がイミュータブルだから当然その中の要素が変更されないことは保証されていて、よくオブジェクトをシャローコピーするときに発生する「参照元が意図せず変更されたせいでコピー先の挙動がおかしくなった」というようなことは発生しません。

とはいえ、もちろんnew_arr用の配列インスタンス自体は新しく作成されている為、ミュータブルに比べてメモリを食い気味になってしまうというのは間違ってはいません。そこは実装する処理の内容や実行環境に応じて、適宜イミュータブルにすべきか否かを判断する必要があります。

 

まとめ

まとめるほどのこともないんですが、このポストをきっかけにイミュータブルなプログラミングにチャレンジしていただければ嬉しいなぁと思います。ではでは。