Thursday, January 3, 2013

Success Using Scala to Create a DSL

Update (March):  I've given up on the idea of creating DSL's for every business domain I work in.  I've concluded that Scala is not that scalable (at least not yet).  The original January post follows:

It took three rounds of making mistakes, but I finally have something elegant.

The primary goal was to represent a route between to airports using natural language.  For example, I wanted to be able to write the following line of code:

val routeFromDetroitToPhilly = "DTW" to "PHL"

I also wanted my model code to be pristine.  I wanted to separate any syntactic sugar from my model code, and to avoid any circular dependencies between model objects.  

Here are my model objects:

case class Airport(code: String)
case class Route(origin: Airport, destination: Airport)

A route links origin and destination Airports.  The Airport is only dependent on String.  This is good.  There was problem introduced, however, when I tried to use the "to" keyword to create a Route from two Airports.  You can see the issue if I re-write the desired code in a way that exposes some of the magic:


val routeFromDetroitToPhilly = Airport("DTW").to(Airport("PHL"))

My initial attempts to implement the "to" keyword involved introducing a circular dependency between Airport and Route:


case class Airport(code: String) {
  def to(destination: Airport) = Route(this, destination)
}
case class Route(origin: Airport, destination: Airport)

This worked, but is nasty.  Route depends on Airport and Airport depends on Route.  My first attempt was even worse.  I tried adding the "to" method for route creation though inheritance.  Not a good idea.  I'm too ashamed to publish that failed attempt.

I finally figured out that I could put my syntactic sugar in a model helper class:

object ModelHelper {

  class AirportRouteBuilder(origin: Airport) {
    def to(destination: Airport): Route = {
      Route(origin, destination)
    }
  }
  
  implicit def stringToAirport(code: String) = Airport(code)
  
  implicit def stringToAirportRouteBuilder(airportCode: String) = new AirportRouteBuilder(Airport(airportCode))
  
  implicit def airportToAirportRouteBuilder(airport: Airport) = new AirportRouteBuilder(airport)
}

Now when I write this code:


val routeFromDetroitToPhilly = "DTW" to "PHL"

...Scala resolves the code as follows:
  • The function "to(...)" is invalid for the String "DTW", but we have an implicit method "stringToAirportRouteBuilder()" that creates an AiportRouteBuilder object that has a "to(...)" method
  • Call "stringToAirportRouteBuilder("DTW")" to create an AirportRouteBuilder object
  • Call "StringToAirport("PHL")" to create the Airport object needed for "AirportRouteBuilder.to(...)"
  • Call the "to()" method on AirportRouteBuilder to construct a new Route object that links the origin and destination airports
This is pretty mind-blowing stuff.  It seems too complex and magical, but I believe it's a way of thinking that we can get used to.  It's like eating ethnic food for the first time or meeting an ugly person for the first time.  After the initial shock, you get used to it if you allow yourself the opportunity to appreciate.


4 comments:

  1. Just don't marry it, no matter how cool it seems after the initial shock.

    As a software craftsman, I like readability first, but ease-of-modification is a close second (if not co-equal) concern. I think that your pairing partner's initial thought that it was somewhat "fragile" was valid.

    "Fragile" can mean to me that it's difficult or opaque to use, which seems to be the case here.

    Still, really good stuff. Now you should do it in Groovy. :-)

    ReplyDelete
  2. I hear you, Erik. I'm taking an angle with this code base that I would never take with a code base targeted for production. My primary concern is testing out Scala's charter as a SCAlable LAnguage. Does Scala deliver? As a consumer of DSL's, it seems so. As a DSL author, it's a battle.

    Similar to Java or Javascript frameworks, I'm sure that 98% of the time it is better to consume a DSL then to create one. Also similar to authoring frameworks, it's something that everyone should try -- but throw it away when you are done.

    It would be an interesting exercise to create a broken DSL and ask people to figure out how to fix it. Once people develop the muscle to modify DSL's, their perspective on maintainability is subject to change.

    ReplyDelete
  3. At last night's Scala Enthusiasts Group, Diane Marsh said that implicit conversion should be used very judiciously. I'm ready to delete the "Airplane" code base and burn the associated GIT repository. This is dangerous ground.

    ReplyDelete
  4. Now that I've learned more scala and grown up a bit, I realize that I was developing an
    "internal" DSL. For the airline DSL, I probably really wanted either an external DSL or a well-defined API with a little bit of internal DSL sugar. Trying to build an internal DSL from the ground up is probably a bad idea.

    ReplyDelete