Scala - 使用最小语法设计 DSL

ak.*_*ak. 4 dsl scala anonymous-function

我希望在 Scala 中设计一个 DSL,它的语法可能最少。它旨在供不了解 Scala 但可以利用 Scala 类型系统进行验证和错误检查的用户使用。在我的脑海中,DSL 是这样的:

outer {
    inner(id = "asdf") {
        value("v1")
        value("v2")
    }
}
Run Code Online (Sandbox Code Playgroud)

这个剪断应该产生一个这样的值:

Outer(Inner("asdf", Value("v1") :: Value("v2") :: Nil))
Run Code Online (Sandbox Code Playgroud)

给定数据结构

case class Outer(inner: Inner)
case class Inner(values: List[Value])
case class Value(value: String)
Run Code Online (Sandbox Code Playgroud)

这个想法是inner函数只在闭包之后可用outervalue函数只在闭包之后可用inner,等等。这就是以下不会编译:outer { value("1") }

我怎样才能实现这样的事情?最后,数据结构不需要是不可变的,只要它是强类型的,它就可以是任何东西。

我对 Scala 宏不熟悉,但我可以用宏来解决这个问题吗?


到目前为止,我最接近的是以下实现:

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)
Run Code Online (Sandbox Code Playgroud)

如何摆脱匿名函数注释(即o =>o.等)?

或者,有没有办法将其outer视为new Outer(在这种情况下,以下代码块将被视为构造函数,我将能够调用成员函数)?

0__*_*0__ 5

正如你所注意到的,它归结为

有没有办法把它outer当作new Outer

不幸的是,答案是否定的。我认为这在实验性的 Scala 虚拟化分支中是可能的。就个人而言,我认为new关键字在 Scala 中也很烦人。


我只看到两种解决方案。

  • 使用宏或编译器插件
  • 使用全局可变构建器对象

我知道有两个项目可以使用第一种方法为您完成这项工作:

我试过前者。我克隆的资源库,并改变scalaVersionproject/build.scala"2.11.6"(而不是快照)。您可以使用sbt sandbox/console.

这个想法是定义带有标记参数的函数,@Implicit这样您就可以将 DSL 树的外部和内部部分“粘合”在一起:

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
}
Run Code Online (Sandbox Code Playgroud)

例子:

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

瞧!


没有插件/宏的解决方案是设置例如ThreadLocal构建器,但是你没有编译时安全:

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
}
Run Code Online (Sandbox Code Playgroud)

例子:

scala> inner(id = "asdf") { value("v1"); value("v2") }
res1: Inner = Inner(asdf,List(Value(v1), Value(v2)))
Run Code Online (Sandbox Code Playgroud)