LoginSignup
13
16

More than 5 years have passed since last update.

不変性と再代入不可能性、或いは「valかつmutable」と「varかつimmutable」について

Last updated at Posted at 2016-12-07

よく言われる「変数の再代入不可能性」と「不変オブジェクト」とについて、考えてみましょう。

※ この記事はScalaの知識を前提に書かれています。

※ このテーマについては既に記事があります。
http://qiita.com/takc923/items/f62c173beacee280c369

再代入不可能性

Scalaをちょっと学んだ事がある方なら、変数を宣言するのにvarとvalがある事はご存知だと思います。
そして、「いいか、何はともあれvarは使うな。valだけ使えばいい」みたいな事を言われた事がある方も多いかと思います。
(これとセットで「nullは使うな。Option型を使え」も言われた事があるでしょう。)

というわけで、valとvarについて見てみましょう。

再代入

多くの(所謂「関数型」と呼ばれない)言語では、変数に再代入する事ができます。

#include <iostream>

int main(void)
{
    int x = 1;
    std::cout << x << std::endl; // 1

    x = 42;
    std::cout << x << std::endl; // 42

    return 0;
}
[1] pry(main)> x = 1
=> 1
[2] pry(main)> x = 42
=> 42

これに対して、関数型と呼ばれる言語では変数の再代入ができない事が多いです。

Prelude> :{
Prelude| x = 1
Prelude| x = 42
Prelude| :}

<interactive>:3:1: error:
    Multiple declarations of x
    Declared at: <interactive>:2:1
                 <interactive>:3:1
1> X = 1.
1
2> X.
1
3> X = 42.
** exception error: no match of right hand side value 42

ややこしい事を言うと、関数型言語では宣言的に束縛しているのだとか、それでも同じ名前の変数を再度宣言する事で似たような事はできるとか、色々あるのですが、今回はとりあえず再代入不可能という事にしておきます。

さて、上を踏まえた上でScalaのvarとvalを見ると、非関数型言語の再代入可能な変数と、関数型言語の値を束縛する変数にそれぞれ似ているとわかります。

scala> var x = 1
x: Int = 1

scala> x = 42
x: Int = 42
scala> val x = 1
x: Int = 1

scala> x = 42
<console>:12: error: reassignment to val
       x = 42
         ^

で、Scalaを学ぶと「varではなくvalを使え」と度々言われる事になるのですが、では何故再代入が禁止されているvalを使った方が良いのでしょうか?

再代入の危険性

再代入可能だと危険だ、とよく言われます。

x = 1

x = 42

x = 999

puts x #=> 999

別に危なくは無いように見えるでしょうか?
確かに、コードが短ければ、かつ処理が単純であれば何の問題もありません。

では次のコードを見て下さい。

#include <iostream>

int main(void)
{
  auto x = 1;

  for (auto i = 0; i < 10; ++i) {
    switch (x) {
      case 1:
        x = 5;
        break;
      case 2:
        while (1) {
          case 3:
            ++x;
          case 4:
            if (x > 4) break;
        }
      if (i % 2 == 1) {
        case 5:
          x = 7;
          break;
      } else {
        case 6:
          x = 1;
          break;
      }
      case 7:
      case 8:
        x = 9;
        break;
      case 9:
        x = 4;
        break;
      default:
        x = 0;
    }
  }

  std::cout << x << std::endl; // ???
  return 0;
}

さてこれは何を出力するでしょうか?

……まぁこれは明らかに作為的です。例としては不適切でしょう。
ただ、ここで見ていただきたいのは、 std::cout << x << std::endl; でxの値がどうなっているかを知るために、xの宣言 auto x = 1; からの全てのコードを見る必要がある、という事です。

以下のコードを見て下さい。

str = ""
if cond
  arr.each do |e|
    str = e if func(e)
  end
end

case str
when some_regex
  str = ans1
when other_regex
  str = ans2
end

puts str

最後の puts str が何を出力するか、一見すると分かり辛いですよね。

これは、コード中の str 変数が指す値の意味が場所によって変わってしまっているからです。
そこで、再代入を(あまり)しないコードに書き換えてみます。

str_orig =
  if cond
    # arr.reduce("") { |seed, e| func(e) ? e : seed }
    tmp_str = ""
    arr.each do |e|
      tmp_str = e if func(e)
    end
    tmp_str
  else
    ""
  end

str =
  case str_orig
  when some_regex
    ans1
  when other_regex
    ans2
  end

puts str

上のコードと見比べると、どうでしょうか?

(if文やcase文が値を返す事に馴染んでいないと、逆に分かり辛いコードだなぁ、と思われるかもしれないのですが、そこは単に慣れの問題だと思います。)

例えば変数strに想定したのと違う値が入っていたとして、その原因を探すのに、どちらの方がやりやすいと感じられるでしょうか?

まぁ感じ方は人それぞれだったりするのですが、少なくとも再代入を行わないコードの方が、処理の過程を後ろから辿りやすくなる事は間違いないでしょう。

上のコードではだいたい変数strについてのみ処理していますが、もし他にも変数が複数個出てきて、それぞれ何らかの処理が加えられていたとしたら、処理の流れを追うのはさらに難しくなります。

再代入を行った場合、今見ている変数が「どこで代入されたのか」を確かめる為に、同じ名前の変数の代入を全て見ていかなければならないのに対し、再代入が禁止されていれば代入箇所は一つだけなので、その代入箇所から調査箇所までの間を調べるだけで足ります。
変数の定義元からさらに遡る事もあるでしょうが、注目する範囲を短くする事ができ、コードの文脈を把握しやすくなります。

再代入不可能の保証

Rubyには「変数の再代入を禁止する保証」がありません(静的なコード解析やLintツールで補助を受ける事は可能かもしれません)。
対して、再代入禁止を保証する修飾子を持つ言語も数多くあります。例えばC++のconst、Javaのfinal等がそれですね。

int main()
{
  const int i = 1;

  // i = 42; // コンパイルエラー!

  return 0;
}

最近のJavaScript(ES2015以降)には、変数の宣言に var の他、 let const が用意されました。この内、 const を使うと再代入が禁止されます。

var x = 1;

x = 42; // 可能

const y = 2;

// y = 42; // エラー!

これら 再代入禁止 を宣言する事ができる言語だと、積極的に変数の再代入を禁止する事で、不用意な、或いは不注意な(偶然変数名が被ってしまった、など)再代入を防ぐ事ができ、見通しの良いコードを書く事が可能となります。

Scalaで、varよりvalを使えと言われる理由もこれです。

上に挙げた書き換えを見ても分かる通り、大抵のコードでは、実は 再代入は必須ではありません 。ですので、可能な限り再代入不可能な変数を扱う事で、コードの質を上げる事ができます。

ただ、言語によっては、再代入を使わないとかえって複雑な読み辛いコードになる事もあるので、そのあたりは状況に応じる必要があるでしょうか。

再代入を禁止するのはあくまでコードを読みやすく、理解しやすくする為なので、再代入を嫌う余りコードの可読性が損なわれるような事があっては元も子もありません。目的を見失わないようにしていきましょう。

不変性

さて、次は不変性の話に移りましょう。

再代入不可能性も「変数が変わらない」という意味では不変性と呼べるかもしれませんが、ここで言う不変性とは「オブジェクトの不変性」、つまりオブジェクトの値が変更されないという意味です。
この性質は イミュータブル と呼ばれたりします。

フィールドの値が変更可能な、つまり可変なオブジェクトの挙動は以下のようなものです。

class Hoge
  attr_reader :fuga

  def initialize(fuga)
    @fuga = fuga
  end

  def mutate!(fuga)
    @fuga = fuga
  end
end

hoge = Hoge.new("fuga")
hoge.fuga #=> fuga

hoge.mutate!("piyo")

hoge.fuga #=> piyo 変更されている!
#include <iostream>

class Hoge
{
public:
  char* fuga;

  Hoge(char*);
  void mutate(char*);
};

Hoge::Hoge(char* fuga): fuga(fuga)
{}

void Hoge::mutate(char* fuga)
{
  this->fuga = fuga;
}

int main()
{
  auto hoge = new Hoge("fuga");
  std::cout << hoge->fuga << std::endl; // fuga

  hoge->mutate("piyo");

  std::cout << hoge->fuga << std::endl; // piyo 変更されている!

  return 0;
}

これに対して不変な、イミュータブルなオブジェクトは、自身のフィールドを変更する事ができません。例えばScalaのcase classで作られるオブジェクトは全てのフィールドが自動的にvalで宣言される事により、イミュータブルになります。

case class Person(name: String, age: Int)

val cadet1 = Person("John", 18)

// cadet1.name = "Bob" // コンパイルエラー!
// cadet1.age = 24 // コンパイルエラー!

「外から変更できない」というだけでなく、勿論クラス内からも変更する事はできません。

case class Person(name: String, age: Int) {
  def grow(): Unit = {
    // this.age = this.age + 1 // コンパイルエラー!
  }
}

では、「同じPersonで、年齢に1足したものが欲しい」という時はどうすればいいでしょうか?
そういう時はコピーしましょう。

case class Person(name: String, age: Int) {
  def grow = this.copy(age = this.age + 1)
}

このように、フィールドを変更せず、一部が異なるものが欲しい場合は コピーする というのが不変オブジェクトの基本的な取り扱いになります。

プリミティブ

C++やJavaにはプリミティブ型が存在します。プリミティブ型は変更不可能で、この挙動はまさしく不変なオブジェクトと同じように見えます(と言うより、不変オブジェクトはプリミティブ型と同じように扱う事ができる、といった方が正しいでしょうか)。

Rubyのようなミューテーションが基本の言語でも、数値型のような他言語のプリミティブに相当する型はイミュータブルです。

Ruby の Fixnum クラスは immutable です。 つまり、オブジェクト自体を破壊的に変更することはできません。

Rubyリファレンスマニュアル

不変オブジェクトの素晴らしさ

不変、イミュータブルなオブジェクトの利点は何でしょう。
それは、一度宣言されてしまえば、その後いかなる処理を経ようとも、値を変更される事が無いという事です。

class Number(var x: Int)

val number = new Number(42)

func1(number)
func2(number)
func3(number)

println(number.x) // ???

さて、このコードは最後に何を出力するでしょうか?

答えは、 func1, func2, func3の定義を見ないと何もわからない です。

何故ならNumber型はミュータブルなオブジェクトであり、そのフィールドの値は引数として渡された関数の先でも変化され得るからです。

しかし以下のコードであれば、

case class Number(x: Int)

val number = Number(42)

func1(number)
func2(number)
func3(number)

println(number.x) // 42!

出力される値は間違いなく42です。
何故ならcase classのコンストラクタで宣言された全てのフィールドはvalで、不変だからです。変数numberがどれだけの関数の間を盥回しにされようとも、そのフィールドが変更されていない事はコンパイラが保証してくれます。

これが不変オブジェクトの利点です!
オブジェクトが不変である事、フィールドが変更されない事が保証されているだけで、コードの理解は容易になり、デバッグはずっと簡単になるのです。

不変性の保証

オブジェクトが不変である事はどのようにしてわかるでしょうか?
関数型言語のデータ型は、標準で不変である事が多いです。
HaskellやErlangといった言語を使えば、何もせずともイミュータブルなオブジェクトの恩恵を受ける事ができます。

他の言語ではどうでしょうか。例えばScalaのcase classは、コンストラクタで何も修飾せずに宣言した変数を全てvalで宣言されたものとするので、素直に宣言したcase classはイミュータブルと扱えます。

case class Room(id: Long, name: String)

RubyやJavaScriptにはfreezeメソッドがあります。これを使う事でオブジェクトを「凍結」する事ができ、イミュータブルな状態にする事ができます。

obj = { s: "str", i: 0 }
obj[:i] = 42

obj
#=> { s: "str", i: 42 }

obj.freeze
# obj[:i] = 999 # RuntimeError: can't modify frozen Hash
const obj = { s: 'str', i: 0 }
obj.i = 42

obj
//=> { s: 'str', i: 42 }

Object.freeze(obj)
obj.i = 999 // strictモードならこの時点で例外
obj
//=> { s: 'str', i: 42 }

イミュータブルなオブジェクトを使う事で、誤ってオブジェクトを変更してしまうような事態を防ぐ事ができます。ただしRubyやJavaScriptのような動的言語は実行時例外が飛んでしまうので、コンパイル時にロジックをチェックする静的言語に比べて安全性はやや劣ると言えるかもしれません。

「浅い」不変性と「深い」不変性

HaskellやErlangのような、元からイミュータブルなデータ型を持つ言語にとって、不変性の浅さ、深さなどという事は問題になりません。

一方で、上で挙げたScala、Ruby、JavaScriptの不変性は全て浅い不変性です。

class Name(var first: String, var last: String)
case class Student(name: Name, grade: Int)

val student = Student(new Name("John", "Doe"), 3)

student.name.first = "Bob" // 変更可能
obj = { point: { x: 1, y: 1 }, mark: "*" }
obj.freeze

obj[:point][:x] = 2 # 変更可能

他方、D言語ではその名も immutable 修飾子を使う事で、オブジェクトをイミュータブルにする事ができます。この不変性は 深い 不変性で、そのオブジェクトのフィールドのみならず、フィールドから参照するオブジェクトまでも、イミュータブルである事を保証します。

struct Employee {
  string name;
  int age;
}

class Department {
  string name;
  Employee[] employees;

  this(string name, immutable Employee[] employees) immutable {
    this.name = name;
    this.employees = employees;
  }
}

void main()
{
  auto emp1 = Employee("taro", 26);
  auto emp2 = Employee("jiro", 25);

  auto dep = new immutable Department("dev1", [emp1, emp2]);

  // dep.name = "dev2"; // コンパイルエラー!

  // dep.employees[0].name = "sabro"; // こちらもコンパイルエラー!
}

不変、かつ再代入不可能

さて、「再代入不可能」と「不変」について見てきました。
どちらも変更を許さない事で、データを安全に取り扱う手伝いをしてくれるものでした。

では、プリミティブや不変なオブジェクトが再代入不可能な変数に束縛されるとどうなるでしょうか?
これは 定数 となります。
つまりプログラムが書かれた段階から、いかなる状況であっても決して変更されない、岩に刻み付けられたかのように安定した値で、コーディング中で最も信頼に足るものです。

val pi = 3.14 // 決して変わらない!

安全な変数 × 安全な値 = 極めて安全

という事ですね。

プログラム中に出てくる全ての値を定数にする事ができれば、健全性は保証されたようなものです。

変更を要する場合

とはいえ、どうしても値を変更しなければならない事もあるでしょう。
例えばDBからの結果をキャッシュするオブジェクトを考えます。

class UserCache
  def initialize()
    @cache = {}
  end

  def fetch(key)
    user = @cache[key]
    return user if user

    @cache[key] = User.find_by(key: key)
  end
end

これをScalaで、出来る限り安全に記述する場合、2つの書き方があります。

可変だが再代入不可能

1つは、 可変オブジェクトを、再代入不可能な変数に束縛するやり方です。

class UserCache(userDao: UserDao) {

  import scala.collection.mutable

  private val cache = mutable.Map.empty[UserKey, User]

  def fetch(key: UserKey): Option[User] =
    cache.get(key).orElse {
      val maybeUser = userDao.findBy(key)
      maybeUser.foreach { user => cache += (key -> user) }
      maybeUser
    }
}

これを「valかつmutable」と表現してみます。

再代入可能だが不変

そしてもう1つが、 不変オブジェクトを再代入可能な変数に代入するやり方です。

class UserCache(userDao: UserDao) {

  private var cache = Map.empty[UserKey, User]

  def fetch(key: UserKey): Option[User] =
    cache.get(key).orElse {
      val maybeUser = userDao.findBy(key)
      maybeUser.foreach { user => cache += (key -> user) }
      maybeUser
    }
}

同様に、これを「varかつimmutable」と表現してみます。

どちらがより良いのか?

状況によります。

……という前置きは当然としても、大抵の場合は、不変オブジェクトを再代入可能な変数に代入する方が、つまり 「varかつimmutable」な方が安全なコードになる と思います。

その理由は、変数が変更され得る範囲の違いです。

ちょっとコードを変えてみましょう。

class UserCache(userDao: UserDao) {

  import scala.collection.mutable

  private val cache = mutable.Map.empty[UserKey, User]

  def fetch(key: UserKey): Option[User] = {
    val cachedUser = getByCache(cache, key)

    cachedUser.orElse {
      getUserAndCache(userDao, key, cache)
    }
  }
}

幾つかの機能を関数に切り出しました。 getByCache がユーザを(存在すれば)キャッシュから取得する関数、 getUserAndCache はDBからユーザを取得し、キャッシュする関数です。

さて、このメソッドを利用してみて、キャッシュが思ったように働かなかったとします。この時、どのような原因が考えられるでしょうか。

  • getByCache で正しくキャッシュからユーザを引けていない
  • getUserAndCache で正しくキャッシュできていない

実はもう一つ考え得る原因があります。

  • getByCache でキャッシュを間違って破壊的に変更してしまっている

ミュータブルなオブジェクトを引数として渡した時、それが「書き換えられない」保証はありません。getByCache は引数を書き換えるような関数には見えませんが、しかし、それを保証するものは何もないのです。
(そして、「一見書き換えないようにみえる」という関数が一番危ないです。コードの挙動を追う時に、見逃しがちになります。)

また、 getUserAndCache も注意するべきです。何故ならこの関数は明確に引数 cache を書き換えるからです。
関数名、周辺のコード、そしてメソッドやクラスの意図からして、この関数が cache を書き換える事は明らかに見えます。しかし、これもまた、 それを保証するものは何もない 点に注意して下さい。

このようにmutableなオブジェクトを利用する時の問題は、主にそれを引数として他の処理に渡す場合で、

  • 書き換えないと思ったら書き換えていた
  • 書き換えると思ったら書き換えていなかった

という事態が発生する事です。
重要なのは、これらの処理は関数の先で行われていて 問題が見え辛い という事です。

では、今度は「immutableかつvar」で書き換えてみます。

class UserCache(userDao: UserDao) {

  private var cache = Map.empty[UserKey, User]

  def fetch(key: UserKey): Option[User] = {
    val cachedUser = getByCache(cache, key)

    cachedUser.orElse {
      val (maybeUser, newCache) = getUserAndCache(userDao, key, cache)
      cache = newCache
      maybeUser
    }
  }
}

やや冗長になっているでしょうか?
getByCachegetUserAndCache は、同名だけれども別関数になっている事に注意して下さい。 getUserAndCache はOptionで包まれたユーザと、新しいキャッシュのタプルを返します。
さて、今度の getByCache ですが、この関数が引数の cache を書き換えない事は明らかです。何故なら cache はimmutableなMapであり、書き換え不可能だからです。

なので、先の例で出た懸念である

  • getByCache でキャッシュを破壊的に変更してしまっている

は、今回では無用の心配です。

また、

val (maybeUser, newCache) = getUserAndCache(userDao, key, cache)
cache = newCache

の2行を見て下さい。
getUserAndCachecache を変更しない代わりに新しいキャッシュを返り値として戻します。
そしてメソッド内で、 明示的に 変数を差し替えて、キャッシュを更新しています。
この2行が、キャッシュを更新する処理である事は明らかです。もしキャッシュが想定していたものと違った場合、その原因は getUserAndCache 関数にあるとすぐにわかります。

つまり、問題の切り分けが容易になるのです。

テストを考える

getByCachegetUserAndCache のテストを書く事を考えてみましょう。

ミュータブルな引数を使った場合、このようなテストでしょうか。

// getByCache
val cache = origCache.clone

val ret = getByCache(cache, key)

assert(ret == Some(expectedUser))
assert(cache == origCache)

// getUserAndCache
val ret = getUserAndCache(userDao, key, cache)

assert(ret == Some(expectedUser))
assert(cache == expectedCache)

副作用があるので、各々のメソッドをテストする際は、 cache の確認も行わなければなりません。
が、その必要性は実装を見ないとわかりません。

一方イミュータブルな引数を使うと、

// getByCache
val ret = getByCache(cache, key)

assert(ret == Some(expectedUser))

// getUserAndCache
val (retUser, retCache) = getUserAndCache(userDao, key, cache)

assert(retUser == Some(expectedUser))
assert(retCache == expectedCache)

引数と返り値に関してのみテストを行えば良くなります。
わかりやすいテストにする事ができます。

まとめ

以上、再代入の禁止や不変性に関する解説をしてみました。
コーディングにおいてこういう制約を課すのは、プログラムの可動部分を減らす為です。現実の機械と同じように、可動部分があればプログラムも壊れやすくなります。

定数、プリミティブ、イミュータブル、再代入不可能な変数等を活用する事で、プログラムの可動部分を減らし、より単純な、理解のしやすい、頑強な、デバグしやすいプログラムを書く事ができます。もし言語にこの手の機能が備わっているならば、是非とも活用してみましょう!

13
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
16