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.
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(), ?)))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With