Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I refer to a variable while assigning a value to it, whilst retaining immutability?

Tags:

I'm fiddling around on my Sunday afternoon and am trying to create a 'room' structure of sorts. Basically, a Room object has a number of exits, which each refer to other Rooms. Now, the first thing I'm trying to create is two Rooms connected to each other, preferably in a single assignment statement. Like this:

case class Room(title: String, exits: Map[Direction.Direction, Room])

val firstRoom = Room("A room", Map(North -> Room("Another room", Map(South -> firstRoom))))

Ergo: Room one has an exit North to room two, room two has an exit South back to room one.

However, as you can imagine, this goes wrong: the firstRoom val is not defined while creating it, so trying to refer to it during its assignment won't work.

I'm pretty sure this is true for most, if not all programming languages. My question: How do I solve this without making my Room object mutable? I can simply create a few Room objects and add the exits to them afterwards, but that makes the Rooms mutable, and as a personal exercise I try to avoid that.

like image 943
cthulhu Avatar asked Jan 22 '12 15:01

cthulhu


1 Answers

Non-Recursive

I think your best option is to do something like this

object Rooms {
  case class Room(title: String) {
    def exits = exitMap(this)
  }
  val first:Room = Room("first")
  val second:Room = Room("second")

  private val exitMap = Map(first -> Map("S" -> second), second -> Map("N" -> first))
}

scala> Rooms.first.title
res: String = first

scala> Rooms.first.exits
res: scala.collection.immutable.Map[java.lang.String,Rooms.Room] = Map(S -> Room(second))

It is perfectly immutable and you avoid nasty recursions.

Recursive

Constructing a recursive structure takes much more care as Scala is not lazy by default. Specifically, it is not possible to create a lazy or call-by-name case class parameter. So, we’ll have to resort to a specialised data structure for this.

One option could be to use Streams:

case class LazyRoom(title: String, exits: Stream[LazyRoom])

object LazyRooms {
  lazy val nullRoom: LazyRoom = LazyRoom("nullRoom", Stream.empty)
  lazy val first: LazyRoom = LazyRoom("first", nullRoom #:: second #:: Stream.empty)
  lazy val second: LazyRoom = LazyRoom("second", nullRoom #:: first #:: Stream.empty)
}

scala> LazyRooms.first.exits(1).title
res> String: second

To be on the save side, I prefixed a dummy room before each Stream to avoid premature access. (A Stream is only lazy in its tail but not in its head.) A dedicated data structure might be able to avoid this.

Cleaned up version

We can do better with a call-by-name helper function to do the dirty work:

case class LazyRoom(title: String, exitMap: Stream[Map[String, LazyRoom]]) {
  def exits = exitMap(1) // skip the Streams empty head
}

def _exitMap(mappedItems: => Map[String, LazyRoom]) = {
  Map[String, LazyRoom]() #::
  mappedItems #::
  Stream.empty
}

object LazyRooms {
  lazy val first: LazyRoom = LazyRoom("first", _exitMap(Map("South" -> second)))
  lazy val second: LazyRoom = LazyRoom("second", _exitMap(Map("North" -> first)))
}

scala> LazyRooms.first.exits
res: Map[String,LazyRoom] = Map(South -> LazyRoom(second,Stream(Map(), ?)))
like image 109
Debilski Avatar answered Sep 21 '22 13:09

Debilski