Y's note

Web技術・プロダクトマネジメント・そして経営について

本ブログの更新を停止しており、今後は下記Noteに記載していきます。
https://note.com/yutakikuchi/

scalaのimmutable

immutable or mutable, val or var

scalaでcollection(List,Seq,Set,Map,Tupple...)を扱う場合はimmutable(不変) or mutable(可変)を使うかで言語内部でのデータの持ち方が異る。また変数の宣言をval(再割当て禁止) or var(再割当て可能)を使うかで実行可否や挙動が変わる。よって2つの観点(immutable or mutable / val or var)の組み合わせで調査をする必要がある。関数型言語ではvalを利用する事が推奨され、scalaのcollectionはdefaultでimmutableが選択されている。よって自然な組み合わせはval × immutableとなり、変数を定義した後に変数に対しては再割当てが行えないので副作用無く安全な方法とされている。これにより通常valで宣言した変数に対して操作を加えた結果を格納する場合はまた別のvalで宣言する必要があるが、collectionにてmutableを用いてvalにて宣言した場合、模倣的な操作が可能なので挙動を追ってみる。

まずはcollection以外の場合、例えばStringなどはval or varのどちらで宣言しても扱うデータとしてはimmutableなので、元のデータに操作を加えた場合後は新しいデータよる再割当てを行おうとする。以下は再割当て挙動、同一性(ポインタが指し示すものが同じ)、等価性(値が同じ)、についての確認。

最初の例ではvalを使用し再割当てできないもの。immutableなのでデータ更新処理で再割当を行おうとしているが変数側のvalでエラーが表示される。

// valで文字列 scalaを代入。
scala> val a = "scala"
a: String = scala

// valで別の変数にコピー。
scala> val b = a
b: String = scala

// valなので再割当てができない。
scala> a = a + " java"
<console>:8: error: reassignment to val
       a = a + " java"
         ^

// 同一性を確認。2つの変数が同じポインタを指している。
scala> b eq a
res4: Boolean = true

// 同一が確認されているので、等価でもある。
scala> b == a
res5: Boolean = true

// valで宣言した変数に対して操作する場合は新しいvalに代入可能
scala> val c = a + " java"
c: String = scala java

次の例はvarで宣言し、再割当て可能としている。最初に定義した変数に対して更新を加えて再割当てした結果、元のデータとポインタの指し示しが異なってしまう。これこそimmutableな扱いで元のデータに対して更新は行わずに新しいデータを作成し新しいポインタで管理している。コピーした先のデータはコピー元の更新の影響を受けないので安全な管理が可能となっている。

// varで文字列scalaを代入。
scala> var a = "scala"
a: String = scala

// varでコピーを作成。
scala> var b = a
b: String = scala

// varなので再割当て可能。
scala> a = a + " java"
a: String = scala java

// コピー元とコピー先はそれぞれ違うポインタになっている。
scala> b eq a
res6: Boolean = false

// 等価でもない。aのみ更新されている。
scala> b == a
res7: Boolean = false

// aは更新済み。
scala> a
res8: String = scala java

// bは更新されていない。
scala> b
res9: String = scala

次にcollection.Setの例。まずはvalで宣言した変数に対してimmutableなSetを操作する。valで宣言された変数に対しては再割当てできない、かつimmutableなSetは自身を更新する追加メソッドが無いというエラーが出てしまう。(immutableでもvalで変数宣言するとこのメソッドは問題なく動作するという面白い挙動は以下で説明する。) Setの更新ができていないのでコピー元、コピー先は同一で等価という判定になる。この例がval × immutableなので一番制限が強いものだが、scalaではこの形式が推奨されている。

// scala, javaのSetを定義。
scala> val a = Set("scala", "java")
a: scala.collection.immutable.Set[String] = Set(scala, java)

// コピーを作成する。
scala> val b = a
b: scala.collection.immutable.Set[String] = Set(scala, java)

// pythonをsetに加えようとするがerrorになる。immutableのSetには追加メソッドが用意されていないというエラー。
scala> a += "python"
<console>:9: error: value += is not a member of scala.collection.immutable.Set[String]
              a += "python"
                ^

// 同一性を確認。2つの変数が同じポインタを指している。
scala> b eq a
res7: Boolean = true

// 同一が確認されているので、等価でもある。
scala> b == a
res8: Boolean = true

次に上のことを変数宣言varで行う。valの場合にはSetがimmutableでも自身を更新して要素を追加するメソッドが有効になる。本来これはコンパイルエラーになる問題だが、scalaがsyntax sugarを用いているためコンパイル時にa += "python"を a = a + "python"として解釈して実行している。これはaのポインタを新しいデータとして再割当てしてしているので、Stringの場合と同様にコピー先との同一性は無い。

// scala, javaのSetを定義
scala> var a = Set("scala", "java")
a: scala.collection.immutable.Set[String] = Set(scala, java)

// コピーを作成する。
scala> var b = a
b: scala.collection.immutable.Set[String] = Set(scala, java)

// pythonをsetに加えようとするが処理が行われる。valの際はエラーとなったがscalaが文法を解釈。また元のデータに対して更新を行うのではなく、新しく割当を行っている。
scala> a += "python"

// コピー元とコピー先はそれぞれ違うポインタになっている。
scala> b eq a
res10: Boolean = false

// 等価でもない。aのみ更新されている。
scala> b == a
res11: Boolean = false

// aは更新済み。
scala> a
res12: scala.collection.immutable.Set[String] = Set(scala, java, python)

// bは更新されていない。
scala> b
res13: scala.collection.immutable.Set[String] = Set(scala, java)

次に変数の宣言をvalで行いSetをmutableとして指定する。mutableの場合は自身のデータ更新が可能なのでポインタの指し示す先が変わることが無い。よって変数がvalであっても再割当てを行わないので、自身のデータがそのまま更新され、更にはコピー先のデータにも依存して内容が反映される。

// scala, javaのSetを定義。
scala> val a = scala.collection.mutable.Set("scala", "java")
a: scala.collection.mutable.Set[String] = Set(java, scala)

// コピーを作成する。
scala> val b = a
b: scala.collection.mutable.Set[String] = Set(java, scala)

// pythonをsetに加えようとするが処理が行われる。変数に対しては再割当ては行われず、自身のデータを更新している。
scala> a += "python"
res14: a.type = Set(python, java, scala)

// 同一性を確認。2つの変数が同じポインタを指している。
scala> b eq a
res15: Boolean = true

// 同一が確認されているので、等価でもある。
scala> b == a
res16: Boolean = true

// aとbの両方が更新されている。
scala> a
res17: scala.collection.mutable.Set[String] = Set(python, java, scala)

// aとbの両方が更新されている。
scala> b
res18: scala.collection.mutable.Set[String] = Set(python, java, scala)

最後に変数をvarで宣言してもvalと同じ結果が得られるが、意味が少し異なる。varの場合は自身のデータも更新と再割当ての両方を行っている。ただしmutableで割当てとしての変更が無いため同一のものを再割当している。

// scala, javaのSetを定義。
scala> var a = scala.collection.mutable.Set("scala", "java")
a: scala.collection.mutable.Set[String] = Set(java, scala)

// コピーを作成する。
scala> var b = a
b: scala.collection.mutable.Set[String] = Set(java, scala)

// pythonをsetに加えようとするが処理が行われる。自身のデータを更新しつつ、再割当てを行っている。
scala> a += "python"
res19: scala.collection.mutable.Set[String] = Set(python, java, scala)

// 同一性を確認。2つの変数が同じポインタを指している。
scala> b eq a
res20: Boolean = true

// 同一が確認されているので、等価でもある。
scala> b == a
res21: Boolean = true

// aとbの両方が更新されている。
scala> a
res22: scala.collection.mutable.Set[String] = Set(python, java, scala)

// aとbの両方が更新されている。
scala> b
res23: scala.collection.mutable.Set[String] = Set(python, java, scala)
まとめ
  • varで変数宣言した場合には同一の変数に対し再割当て可能。
  • valで変数宣言した場合には同一の変数に対し再割当てはできない。
  • collection.immutableを使用するとデータの更新を行う際は新しいcollectionデータを作成し割当を行おうとする。
  • collection.mutableを使用するとデータの更新を行う際は自身を更新して新しいcollectionデータの作成は行わない。
  • varで宣言した変数に対してcollection.immutableの追加更新メソッド(+=)をコンパイラが良しなに解釈して実行する。ただしvalで宣言するとコンパイルエラーが出力される。理由はcollection.immutableにて新しいデータの作成し再割当てを行おうとしているから。
  • 制限の強い順に組み合わせを上げるとすると val × immutable, var × immutable, val × mutable, var × mutableとなり、イメージとしてはimmutable or mutableの使い分けの判断が重要となる。
  • scalaドキュメントにもcollectionのimmutable/mutableの違いが分かりやすい説明として載っている。(下記引用)Collections - 可変コレクションおよび不変コレクション - Scala Documentation はてなブックマーク - Collections - 可変コレクションおよび不変コレクション - Scala Documentation
    • Scala のコレクションは、体系的に可変および不変コレクションを区別している。可変 (mutable) コレクションは上書きしたり拡張することができる。これは副作用としてコレクションの要素を変更、追加、または削除することができることを意味する。一方、不変 (immutable) コレクションは変わることが無い。追加、削除、または更新を模倣した演算は提供されるが、全ての場合において演算は新しいコレクションを返し、古いコレクションは変わることがない。