4

Playing about with a DSL in Scala, so lets say I have something like this:

house {
  floor {
    bedroom("kids)
    bedroom("master")
  }
  floor {
    kitchen()
  }
}

Now what I want is at each nested block it to have a reference or be referencing functions on the enclosing block. Eg so the effect is that floor is added to the house, bedroom to the floor etc.

Currently I do this in a horrible fashion of having a global stack that gets updated at each nested level to keep track of the current "context". Also my current version is not typesafe in that I can add a bedroom to a house.

Another previous revision was

house {
  floor {
    bedroom("kids) +
      bedroom("master")
  } +
    floor {
      kitchen()
    }
}

Where each block returned a List of widgets (the + was using an implicit to turn a generic "thing" into a "thing list" so that the next "thing" can be added). The returned list of widgets was then added once the block returned. But I do not like the forced use of + as it gets ugly on many pages worth.

Anyway to meld the two ?

sksamuel
  • 16,154
  • 8
  • 60
  • 108

2 Answers2

2

This approach uses mutable fields to set the child-to-parent relation after the involved objects have been created:

/* Data classes */

class House(val floors: Seq[Floor])
class Floor(val name: String, val bedrooms: Seq[Bedroom]) { var house: House = _}
class Bedroom(val name: String) { var floor: Floor = _ }

/* Factory methods */

def house(floors: Floor*) = {
  val house = new House(floors)
  floors foreach (_.house = house)
  house
}

def floor(name: String)(bedrooms: Bedroom*) = {
  val floor = new Floor(name, bedrooms)
  bedrooms foreach (_.floor = floor)
  floor
}

def bedroom(name: String) = new Bedroom(name)

This allows you to create house structures in a concise and type-safe way as follows:

val myHouse = 
  house(
    floor("first")(
      bedroom("joe")
    ),
    floor("second")(
      bedroom("anna"),
      bedroom("clara")
    )
  )

assert(myHouse.floors(0).house == myHouse)
assert(myHouse.floors(1).house == myHouse)
assert(myHouse.floors(0).bedrooms(0).floor == myHouse.floors(0))
assert(myHouse.floors(1).bedrooms(1).floor == myHouse.floors(1))

It should be fairly easy to factor out common behaviour into some base traits or methods, for example, iterating over the subcomponents to fix the relationships.

Malte Schwerhoff
  • 12,684
  • 4
  • 41
  • 71
  • It would be nice to combine it with http://stackoverflow.com/questions/5682277/how-to-initialize-and-modify-a-cyclic-persistent-data-structure-in-scala in a way such that one can use truly immutable case classes `House(Floor)` and `Floor(String, House)`. That probably requires some named or anonymous intermediate classes. However, I currently don't have time to give it a try. – Malte Schwerhoff Jan 30 '13 at 16:41
  • How does this handle multiple definitions at the same level? Eg two floors together, in that case wouldn't the first floor be lost unless you combine with the second in some collection structure (like my example 2) – sksamuel Jan 30 '13 at 17:01
  • @monkjack Good point, I missed that. I changed my solution accordingly, but the code is unfortunately no longer as concise as in your examples. – Malte Schwerhoff Jan 31 '13 at 08:58
1

Do you really need each block to have a reference to the enclosing block? Or was it just so that you could add the nested block to the parent block? In this case you could simply pass nested blocks to the enclosing block, so to speak:

house (
  floor (
    bedroom("kids"),
    bedroom("master")
  ),
  floor (
    kitchen
  )
)

Using the following definitions:

trait HouseElement
case class house( elements: HouseElement* )
trait FloorElement
case class floor( elements: FloorElement * ) extends HouseElement
case class bedroom( name: String ) extends FloorElement
case object kitchen extends FloorElement

Otherwise, another solution is to rely heavily on anonymous classes (which unfortunately requires to use the new keyword everywhere):

new house {
  new floor {
    new bedroom("kids")
    new bedroom("master")
  }
  new floor {
    new kitchen()
  }
}

Using the following definitions:

import collection.mutable.Buffer
class house {
  val elements = Buffer[Element]()
  trait Element {
    elements += this 
  }        
  class floor extends Element { 
    val elements = Buffer[Element]()
    trait Element {
      elements += this 
    }        
    class bedroom(name: String) extends Element 
    class kitchen extends Element
  }
}
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97