4

I'm looking to design a DSL in Scala that has the least amount of syntax cruft possible. It's meant to be used by users who don't know Scala, but can take advantage of Scala type system for validation and error checking. In my head the DSL looks like this:

outer {
    inner(id = "asdf") {
        value("v1")
        value("v2")
    }
}

This snipped should produce a value like this:

Outer(Inner("asdf", Value("v1") :: Value("v2") :: Nil))

Given data structures

case class Outer(inner: Inner)
case class Inner(values: List[Value])
case class Value(value: String)

The idea is that inner function is only available in the closure following outer, value function is only available withing the closure after inner, etc. That is the following won't compile: outer { value("1") }.

How can I implement something like this? In the end the data structures don't need to be immutable, it can be anything as long as it's strongly typed.

I have no familiarity with Scala macros, but can I solve this problem with macros by any chance?


The closest I have so far is the following implementation:

object DSL extends App {

    def outer = new Outer()

    class Outer(val values: mutable.MutableList[Inner] = mutable.MutableList.empty) {
        def inner(id: String): Inner = {
            val inner = new Inner(id)
            values += inner
            inner
        }
        def apply(func: Outer => Unit): Outer = {
            func(this)
            this
        }
        override def toString: String = s"Outer [${values.mkString(", ")}]"
    }

    class Inner(val id: String, val values: mutable.MutableList[Value] = mutable.MutableList.empty) {
        def value(v: String): Value = {
            val value = new Value(v)
            values += value
            value
        }
        def apply(func: Inner => Unit): Unit = func(this)

        override def toString: String = s"Inner (${values.mkString(", ")})"
    }

    class Value(val str: String) {
        override def toString: String = s"Value<$str>"
    }

    val value = outer { o =>
        o.inner(id = "some_id") { i =>
            i.value("value1")
            i.value("value2")
        }
    }

    println(value)

How can I get rid of the anonymous function annotations (i.e. o => and o., etc.)?

Alternatively is there a way to treat outer as new Outer (in this case the following code block will be treated as constructor and I will be able to call member functions)?

1 Answer 1

5

As you have noticed it boils down to

is there a way to treat outer as new Outer

and unfortunately the answer is no. I think this was possible in the experimental Scala-Virtualized fork. Personally, I think the new keyword is very annoying in Scala, too.


There are only two solutions I see.

  • use macros or a compiler plugin
  • use a global mutable builder object

I am aware of two projects that could do the job for you with the first approach:

I have tried with the former. I cloned the repository and changed scalaVersion in project/build.scala to "2.11.6" (instead of the snapshot). You can drop into the REPL using sbt sandbox/console.

The idea is to define functions that take a parameter marked @Implicit and that way you can "glue" the outer and inner parts of the DSL tree together:

import org.dslparadise.annotations._
import scala.collection.mutable.Builder

case class Outer(inner: Inner)
case class Inner(id: String, values: List[Value])
case class Value(value: String)

def outer(i: Inner) = Outer(i)  // nothing special here

def inner(id: String)
         (body: (Builder[Value, List[Value]] @Implicit) => Unit): Inner = {
  val b = List.newBuilder[Value]  // to "build" the contents of inner
  body(b)
  Inner(id, b.result)
}

def value(x: String)(implicit b: Builder[Value, List[Value]]): Value = {
  val v = Value(x)
  b += v   // side-effect: populate the builder
  v
}

Example:

scala> outer {
     |   inner(id = "asdf") {
     |     value("v1")
     |     value("v2")
     |   }
     | }
res1: Outer = Outer(Inner(asdf,List(Value(v1), Value(v2))))

Voila!


The solution without plugin/macros would be to set-up for example ThreadLocal builders, but then you have no compile-time safety:

val values = new ThreadLocal[Builder[Value, List[Value]]]

def inner(id: String)(body: => Unit): Inner = { 
  val prev = values.get()
  values.set(List.newBuilder[Value])
  body
  val v = values.get().result
  values.set(prev)
  Inner(id, v)
}

def value(x: String): Value = { 
  val v = Value(x)
  values.get() += v
  v
}

Example:

scala> inner(id = "asdf") { value("v1"); value("v2") }
res1: Inner = Inner(asdf,List(Value(v1), Value(v2)))
Sign up to request clarification or add additional context in comments.

1 Comment

This is brilliant. I didn't ask this question, but I'm glad I came across this answer. I tried solving the problem, but it never occurred to me how easy it could be with Builder and dsl-paradise! Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.