4

I'm aware that immutability is not always the holy grail. However, since I'm learning Scala for quite a while now, I always try to find an immutable solution at first, especially when it comes to pure "data objects". I'm currently in the progress of finding a method for creating an immutable object graph for a given scenario, but I'm not sure if this is possible at all.

I just want to create the graph once, changes after the creation are not necessary.

Imagine the following scenario:

  • There is only one type: Person.
  • Person objects can have two types of references:
    • there is a unidirected 1-n relationship between a Person and potential children (also of type Person).
    • Furthermore, wifes have husbands and vice versa

The first problem is that the relationship between two spouses is cyclic. Because setting references results in new objects (due to immutability), eventually spouse A is pointing to spouse B_old and spouse B is pointing to spouse A_old. Someone in another posting said that cyclic references and immutability are an oxymoron. I don't think this is always true since spouse A could create spouse B in its own constructor and pass this - but even when using this uncomfortable approach, adding the children references afterwards would change A and B again. The other way round - beginning with children and then linking spouses - results in a similar situation.

At the moment, I think there is no way to do this. but maybe I'm wrong and there are some patterns or workarounds I'm not aware of. If not, is mutability the only solution?

fxlae
  • 905
  • 8
  • 17

2 Answers2

4

I can imagine several tricks how you can create immutable cycle, including, but not limited to:

  • privately mutable class, which is effectively immutable from outside
  • reflection

But the one I like most (and it is truly scala-way) is carefully mixed lazy evaluation and by-name parameters:

object DeferredCycle extends App {

  class C(val name:String, _child: => C) {
    lazy val child = _child
    override def toString: String = name + "->" + child.name
  }

  val a:C = new C("A", b)
  val b:C = new C("B", a)

  println(a)
  println(b)
}

Prints:

A->B
B->A
Aivean
  • 10,692
  • 25
  • 39
  • Thank you, very useful. Just one question: the "forward referencing" you are doing when creating the instance of `a` (referencing `b`) only works because `b` is a property of the `DeferredCycle` object, right? So this would not work if `a` and especially `b` were just local variables inside a method. Or am I wrong at this point? – fxlae Oct 24 '15 at 12:05
  • 1
    @DaniW, you are right, but you can use countless tricks to workaround this limitation. You can create local object inside the method, you can define these classes as `var`s (and then optionally redefine as `val`s), you can create them as elements of some collection, etc. – Aivean Oct 24 '15 at 21:59
  • @Aivean I wonder how the _"privately mutable class"_ could look like. Has it something like a mutable setter that can be used only once, perhabs controlled through a boolean flag? edit: Something like `set(c: Child): Unit = { if(switch) this.child = c; switch = false }` ? – ceran Mar 17 '16 at 19:28
  • @ceran, well, your approach is also viable but it's closer to "validating invariant". By "privately mutable class" I meant literally private setter, that is accessible only from the place where instances are created, like some factory. When instance leaves the factory it can be considered effectively immutable, as setter is private. – Aivean Mar 17 '16 at 22:11
2

To add another perspective, you don't always have to model relations as containment. You could add another level of indirection, such as an opaque identifier.

case class PersonId(id: Int)
case class Person(id: PersonId, name: String, spouse: Option[PersonId], children: Seq[PersonId])

val people: Map[PersonId, Person] = ...

Alternatively, relations don't even need to be members of Person, they could also be maintained externally:

case class PersonId(id: Int)
case class Person(id: PersonId, name: String)

val people: Map[PersonId, Person] = ...
val spouses: Map[PersonId, PersonId] = ...
val children: Map[PersonId, Seq[PersonId]] = ...
JimN
  • 3,120
  • 22
  • 35
  • This was my first approach. What I didn't say in the question is that `Person` has several subtypes. I guess I would lose quite a bit of type safety if referencing is only done by Ids. I would like to upvote your post but I need 15 credits first :/ – fxlae Oct 24 '15 at 12:24