Monday, July 6, 2009

Simple DSL in Scala

Recently I started to use Scala for my hobby-project.I had heard that Scala made it possible to write a code in a way that it looked like a DSL embedded inthe Scala language itself. So I decided to try this feature myself. I needed some concise and convenient API to schedule messages for actors. The following is what I ended up with:

final class TimeSpec[T] (number : Long, action : Long => T) {
def nanoseconds = action (number)
def microseconds = action (number * 1000L)
def miliseconds = action (number * 1000L * 1000L)
def seconds = action (number * 1000L * 1000L * 1000L)
def minutes = action (number * 1000L * 1000L * 1000L * 60L)
def hours = action (number * 1000L * 1000L * 1000L * 60L * 60L)
def days = action (number * 1000L * 1000L * 1000L * 60L * 60L * 24L)
}

final class Trigger (actor : MyActor, payload : Any) {
def in (number : Long) = new TimeSpec[Unit] (number, scheduleIn)
def every (number : Long) = new TimeSpec[Unit] (number, scheduleEvery)

private def scheduleIn (nanos : Long) = Scheduler.inNano (actor, payload, nanos)
private def scheduleEvery (nanos : Long) = Scheduler.everyNano (actor, payload, nanos)
}

final class ActorSchedule (actor : MyActor) {
def payload (payload : Any) = new Trigger (actor, payload)
}

trait MyActor ..... {
protected val schedule = new ActorSchedule (this)
....
}


This code made it possible to schedule messages in a natural way, like:

object TestActor extends MyActor {
schedule payload `Hi in 10 nanoseconds

schedule payload `HowAreYou every 5 seconds

schedule payload `Bye in 5 days

def act () = {
case `Hi => println ("Hello!")
case `HowAreYou => println ("I am fine")
case `Bye => println ("Bye-bye")
}
}

The idea is that schedule is an object of class ActorSchedule. This class has method payload which takes a payload object as its argument. So "schedule payload `Hi" is actually an invocation "schedule.payload(`Hi)". This invocation will create an object of class Trigger. Trigger class has two public methods - in(Long) and every(Long). Because we cannot do anything until a time unit is given, we create an object of TimeSpec class passing to it a method (scheduleIn or scheduleEvery) that is to be run with number of nanoseconds when one of TimeSpec's methods is called. I think this API is concise enough with only small efforts taken to implement it.