Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Internal DSL in Scala: Lists without ","

Tags:

scala

dsl

I'm trying to build an internal DSL in Scala to represent algebraic definitions. Let's consider this simplified data model:

case class Var(name:String)
case class Eq(head:Var, body:Var*)
case class Definition(name:String, body:Eq*)

For example a simple definition would be:

val x = Var("x")
val y = Var("y")
val z = Var("z")
val eq1 = Eq(x, y, z)
val eq2 = Eq(y, x, z)
val defn = Definition("Dummy", eq1, eq2)

I would like to have an internal DSL to represent such an equation in the form:

Dummy {
   x = y z
   y = x z
}

The closest I could get is the following:

Definition("Dummy") := (
    "x" -> ("y", "z")
    "y" -> ("x", "z")
)

The first problem I encountered is that I cannot have two implicit conversions for Definition and Var, hence Definition("Dummy"). The main problem, however, are the lists. I don't want to surround them by any thing, e.g. (), and I also don't want their elements be separated by commas.

Is what I want possible using Scala? If yes, can anyone show me an easy way of achieving it?

like image 487
Wickoo Avatar asked Jan 26 '13 12:01

Wickoo


2 Answers

While Scalas syntax is powerful, it is not flexible enough to create arbitrary delimiters for symbols. Thus, there is no way to leave commas and replace them only with spaces.

Nevertheless, it is possible to use macros and parse a string with arbitrary content at compile time. It is not an "easy" solution, but one that works:

object AlgDefDSL {

  import language.experimental.macros

  import scala.reflect.macros.Context

  implicit class DefDSL(sc: StringContext) {
    def dsl(): Definition = macro __dsl_impl
  }

  def __dsl_impl(c: Context)(): c.Expr[Definition] = {
    import c.universe._

    val defn = c.prefix.tree match {
      case Apply(_, List(Apply(_, List(Literal(Constant(s: String)))))) =>

        def toAST[A : TypeTag](xs: Tree*): Tree =
          Apply(
            Select(Ident(typeOf[A].typeSymbol.companionSymbol), newTermName("apply")),
            xs.toList
          )

        def toVarAST(varObj: Var) =
          toAST[Var](c.literal(varObj.name).tree)

        def toEqAST(eqObj: Eq) =
          toAST[Eq]((eqObj.head +: eqObj.body).map(toVarAST(_)): _*)

        def toDefAST(defObj: Definition) =
          toAST[Definition](c.literal(defObj.name).tree +: defObj.body.map(toEqAST(_)): _*)

        parsers.parse(s) match {
          case parsers.Success(defn, _)  => toDefAST(defn)
          case parsers.NoSuccess(msg, _) => c.abort(c.enclosingPosition, msg)
        }
    }
    c.Expr(defn)
  }

  import scala.util.parsing.combinator.JavaTokenParsers

  private object parsers extends JavaTokenParsers {

    override val whiteSpace = "[ \t]*".r

    lazy val newlines =
      opt(rep("\n"))

    lazy val varP =
      "[a-z]+".r ^^ Var

    lazy val eqP =
      (varP <~ "=") ~ rep(varP) ^^ {
        case lhs ~ rhs => Eq(lhs, rhs: _*)
      }

    lazy val defHead =
      newlines ~> ("[a-zA-Z]+".r <~ "{") <~ newlines

    lazy val defBody =
      rep(eqP <~ rep("\n"))

    lazy val defEnd =
      "}" ~ newlines

    lazy val defP =
      defHead ~ defBody <~ defEnd ^^ {
        case name ~ eqs => Definition(name, eqs: _*)
      }

    def parse(s: String) = parseAll(defP, s)
  }

  case class Var(name: String)
  case class Eq(head: Var, body: Var*)
  case class Definition(name: String, body: Eq*)
}

It can be used with something like this:

scala> import AlgDefDSL._
import AlgDefDSL._

scala> dsl"""
     | Dummy {
     |   x = y z
     |   y = x z
     | }
     | """
res12: AlgDefDSL.Definition = Definition(Dummy,WrappedArray(Eq(Var(x),WrappedArray(Var(y), Var(z))), Eq(Var(y),WrappedArray(Var(x), Var(z)))))
like image 113
kiritsuku Avatar answered Sep 28 '22 11:09

kiritsuku


In addition to sschaef's nice solution I want to mention a few possibilities that are commonly used to get rid of commas in list construction for a DSL.

Colons

This might be trivial, but it is sometimes overlooked as a solution.

line1 ::
line2 ::
line3 ::
Nil

For a DSL it is often desired that every line that contains some instruction/data is terminated the same way (opposed to Lists where all but the last line will get a comma). With such a solutions exchanging the lines no longer can mess up the trailing comma. Unfortunately, the Nil looks a bit ugly.

Fluid API

Another alternative that might be interesting for a DSL is something like that:

BuildDefinition()
.line1
.line2
.line3
.build

where each line is a member function of the builder (and returns a modified builder). This solution requires to eventually convert the builder to a list (which might be done as an implicit conversion). Note that for some APIs it might be possible to pass around the builder instances themselves, and only extract the data wherever needed.

Constructor API

Similarly another possibility is to exploit constructors.

new BuildInterface {
  line1
  line2
  line3
}

Here, BuildInterface is a trait and we simply instantiate an anonymous class from the interface. The line functions call some member functions of this trait. Each invocation can internally update the state of the build interface. Note that this commonly results in a mutable design (but only during construction). To extract the list, an implicit conversion could be used.

Since I don't understand the actual purpose of your DSL, I'm not really sure if any of these techniques is interesting for your scenario. I just wanted to add them since they are common ways to get rid of ",".

like image 40
bluenote10 Avatar answered Sep 28 '22 12:09

bluenote10