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 Room
s. Now, the first thing I'm trying to create is two Room
s 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 Room
s 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 Stream
s:
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