in scala, how to create a list of immutable objects with references among them (when the instances do not know in advance)?

advertisements

This answer https://stackoverflow.com/a/41717310/280393 explains how to create a list of immutable objects with references among them; However, the solution provided requires the objects to be known in advance. How to achieve this when the objects are created on demand?

  case class PersonA(id: Int, name: String, friends: Set[Int])
  val john = PersonA(0, "john", Set(1,2))
  val maria = PersonA(1, "maria", Set(0))
  val georges = PersonA(2, "georges", Set(1))
  val peopleA = Set(john, maria, georges)

  case class PersonB(id: Int, name: String, friends: Set[PersonB])
  // case class PersonB(id: Int, name: String, friends: () => Set[PersonB])

  def convert(peopleA: Set[PersonA]): Set[PersonB] = ???

  val peopleB = convert(peopleA)

  println(peopleB)
  println(peopleB.toList.map(_.friends.size))
  peopleB.toList.map {
    case PersonB(id, name, friends) => friends.size
  }.foreach(println)

So, without modifying the implementation of case class PersonA and val peopleA, how to implement convert?

assuming that two PersonB instances are equal iff their id is equal, one solution would be like this:

class PersonB(val id: Int, val name: String) {
  var friends0: Set[PersonB] = _
  def setFriends(friends: Set[PersonB]) {
    require(friends0 == null)
    friends0 = friends
  }
  def friends: Set[PersonB] = {
    require(friends0 != null)
    friends0
  }

  override def equals(that: Any): Boolean = that match {
    case t: PersonB => t.id == id
    case _ => false
  }

  override def hashCode(): Int = id.hashCode

  override def toString = s"PersonB($id, $name, List(${friends.map(_.id).mkString(", ")}))"
}

object PersonB {
  def apply(id: Int, name: String) = new PersonB(id, name)
  def apply(id: Int, name: String, friends: Set[PersonB]): PersonB = {
    val p = new PersonB(id, name)
    p.setFriends(friends)
    p
  }

  def unapply(p: PersonB): Option[(Int, String, Set[PersonB])] =
    Some((p.id, p.name, p.friends))
}

def convert(peopleA: Set[PersonA]): Set[PersonB] = {
  val peopleB = peopleA.map(p => new PersonB(p.id, p.name))
  val peopleBMap = peopleB.map(p => (p.id, p)).toMap
  peopleA.foreach(p =>
    peopleBMap(p.id).setFriends(p.friends.map(peopleBMap))
  )
  peopleB
}

Is there a simpler way?


Udate Solution based on @sjrd answer:

class PersonB(val id: Int, val name: String, friends0: => Set[PersonB]) {
  lazy val friends: Set[PersonB] = friends0

  override def equals(that: Any): Boolean = that match {
    case t: PersonB => t.id == id
    case _ => false
  }

  override def hashCode(): Int = id.hashCode

  override def toString = s"PersonB($id, $name, List(${friends.map(_.id).mkString(", ")}))"
}

object PersonB {
  def apply(id: Int, name: String, friends: => Set[PersonB]): PersonB =
    new PersonB(id, name, friends)

  def unapply(p: PersonB): Option[(Int, String, Set[PersonB])] =
    Some((p.id, p.name, p.friends))
}

def convert(peopleA: Set[PersonA]): Set[PersonB] = {
  lazy val peopleB: Map[Int, PersonB] =
    (for (PersonA(id, name, friendsIDs) <- peopleA)
      yield (id, PersonB(id, name, friendsIDs.map(peopleB)))).toMap

  peopleB.values.toSet
}


Using this definition of PersonB:

case class PersonB(id: Int, name: String, friends: () => Set[PersonB])

you can write:

def convert(peopleA: Set[PersonA]): Set[PersonB] = {
  lazy val peopleB: Map[Int, PersonB] =
    (for (PersonA(id, name, friendsIDs) <- peopleA)
      yield PersonB(id, name, () => friendsIDs.map(peopleB)).toMap
  peopleB.values.toSet
}

Not tested, but something close to this should work.