不変オブジェクトで循環参照をつくる
お題:
新しい状態を作るにはcopyメソッドで新たなインスタンスを作ります。 そうすると結局お互いに保持している参照が古い状態を指していることになるので、参照を新しいものに付け替えなければなりません。 先の例ではvar属性だったため、再代入することで付けかけることができましたが、valになると新しいインスタンスを作らないと無理なのです。 ということで、これはループしてしまうので解決することはできません。そもそもvalだと相互参照の関連の取り扱いに無理があることがわかります。
http://j5ik2o.me/blog/2013/06/02/ddd-entity-reference/
内容から言えば全然本筋ではないのだけれど、不変オブジェクトで相互参照って不可能なのかなーと思っていろいろやってみたら出来たので書いてみる。
まあコードの中にIDによる参照が入るのがあんまり好きじゃない(そもそもフィールド自体が参照なので)というのもある。(ここの感想らへん参照)
シンプルな例
名前渡し(call-by-name)とlazy valを使うと、それぞれのフィールドが相互に参照し合うオブジェクトがつくれる。
クラス定義。コンストラクタの引数をcall-by-nameにして、その値をlazy valなフィールドの初期化に使っている。
class A(_b: => B) { lazy val b = _b } class B(_a: => A) { lazy val a = _a }
これに対して、以下のようにlazy valな変数を使ってnewしてやれば、相互に参照するオブジェクトがつくられる。
lazy val a: A = new A(b) lazy val b: B = new B(a) // aと、bの中のaは同じ? assert(a == a.b.a) printf("a\t= %s\n", a) printf("a.b.a\t= %s\n", a.b.a) // bと、aの中のbは同じ? assert(b == b.a.b) printf("b\t= %s\n", b) printf("b.a.b\t= %s\n", b.a.b)
出力も見てみる
a = A@13b84286 a.b.a = A@13b84286 b = B@26177587 b.a.b = B@26177587
ちゃんと相互に同じオブジェクトが参照されているようだ。
Employee/Departmentの例
真似して書いてみる
class Employee(val id: Long, val name: String, _department: => Option[Department] = None) { lazy val department = _department def copy(name: String = this.name, department: => Option[Department] = this.department) = new Employee(id, name, department) } class Department(val id: Long, val name: String, _employees: => Seq[Employee] = Seq.empty) { lazy val employees = _employees def copy(id: Long = this.id, name: String = this.name, employees: => Seq[Employee] = this.employees) = new Department(id, name, employees) }
idとnameはvalで、department、employeesは先ほどと同じように、引数をcall-by-nameで受けてlazy valなフィールドに使っている。copyメソッドも3つ目の引数をcall-by-nameにしている以外は同じ。
さて、新たなEmployeeに対してDepartmentをセットしながら、DepartmentのemployeesにそのEmployeeを追加するにはどうしたら良いか。
- Employeeのdepartmentをセット
- DepartmentのemployeesにEmployeeを追加
に加え、
- Departmentのemployeesの各要素のdepartmentを新しく作るDepartmentに差し替え
が必要になる。
関数で書くと次のようになる。
def addEmployeeToDepartment(employee: Employee, department: Department) = { // departmentの現在のemployeesの後ろにemployeeを追加し、 // 各自のdepartmentをnewDepartmentに差し替えたもの lazy val newEmployees: Seq[Employee] = (department.employees :+ employee) map (_.copy(department = newDepartment)) // departmentのemployeesをnewEmployeesに差し替えたもの lazy val newDepartment: Some[Department] = Some(department.copy(employees = newEmployees)) // 新しいDepartmentを戻す newDepartment.get }
:+で後ろに付けるのはコスト大きそうなので本当は逆順にしたりすると思う。
使いやすいようにDepartmentにメソッドの形で付ける。
class Department(val id: Long, val name: String, _employees: => Seq[Employee] = Seq.empty) { lazy val employees = _employees def copy(id: Long = this.id, name: String = this.name, employees: => Seq[Employee] = this.employees) = new Department(id, name, employees) def setEmployees(employees: Seq[Employee]) = { lazy val newEmployees: Seq[Employee] = employees map (_.copy(department = newDepartment)) lazy val newDepartment: Some[Department] = Some(copy(employees = newEmployees)) newDepartment.get } def addEmployee(emp: Employee) = setEmployees(employees :+ emp) }
使ってみる。
val department = new Department(1, "Dev") .addEmployee(new Employee(1, "KATO")) .addEmployee(new Employee(2, "BANDOU")) // departmentとdepartmentに属するemployeeのdepartmentが一致 assert(department.employees.forall(e => Some(department) == e.department)) // employeeはemployeeが属するdepartmentに属する assert(department.employees.forall(e => e.department.get.employees.contains(e)))
ちゃんと相互参照になっているようだ。
感想
今回は1対1、または、1対多の固定的なデータ構造の相互参照を作るという内容だったのでベタに書けたけど、これが内部に相互参照を含む任意のデータ構造に対してそのトポロジを維持しつつ一部を追加したり繋ぎ替えたりする、というような汎用的な形で実現できたらカッコイイかも(ZipperとかLensみたいな感じで。)