Scala中的内部DSL:没有“,”的列表

时间:2013-01-26 12:20:49

标签: scala dsl

我正在尝试在Scala中构建一个内部DSL来表示代数定义。让我们考虑一下这个简化的数据模型:

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

例如,一个简单的定义是:

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)

我希望有一个内部DSL来表示以下形式的等式:

Dummy {
   x = y z
   y = x z
}

我能得到的最接近的是:

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

我遇到的第一个问题是我不能对Definition和Var进行两次隐式转换,因此Definition("Dummy")。然而,主要问题是清单。我不想用任何东西包围它们,例如(),我也不希望它们的元素用逗号分隔。

使用Scala是我想要的吗?如果是的话,有人能告诉我一个实现它的简单方法吗?

3 个答案:

答案 0 :(得分:11)

虽然Scalas语法功能强大,但它不够灵活,无法为符号创建任意分隔符。因此,没有办法留下逗号并仅用空格替换它们。

然而,可以使用宏并在编译时解析具有任意内容的字符串。它不是一个“简单”的解决方案,而是一个有效的解决方案:

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*)
}

它可以用于这样的东西:

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)))))

答案 1 :(得分:5)

除了sschaef的好解决方案之外,我想提一些常用于摆脱DSL列表构造中逗号的可能性。

结肠

这可能是微不足道的,但它有时会被忽视作为一种解决方案。

line1 ::
line2 ::
line3 ::
Nil

对于DSL,通常希望包含某些指令/数据的每一行都以相同的方式终止(与列表相反,除了最后一行之外的所有行都将获得逗号)。有了这样的解决方案,交换行不再会弄乱尾随的逗号。不幸的是,Nil看起来有点难看。

Fluid API

另一种可能对DSL感兴趣的替代方案是:

BuildDefinition()
.line1
.line2
.line3
.build

其中每一行是构建器的成员函数(并返回修改后的构建器)。此解决方案需要最终将构建器转换为列表(可能作为隐式转换完成)​​。请注意,对于某些API,可能会传递构建器实例本身,并且只在需要的地方提取数据。

构造函数API

同样,另一种可能性是利用构造函数。

new BuildInterface {
  line1
  line2
  line3
}

这里,BuildInterface是一个特征,我们只是从界面中实例化一个匿名类。行函数调用此特征的一些成员函数。每次调用都可以在内部更新构建接口的状态。请注意,这通常会导致可变设计(但仅限于构造期间)。要提取列表,可以使用隐式转换。

由于我不了解您的DSL的实际目的,我不确定这些技术中的任何一种对您的场景是否有趣。我只想添加它们,因为它们是摆脱“,”的常用方法。

答案 2 :(得分:1)

这是另一个相对简单的解决方案,它的语法非常接近理想 (正如其他人指出的那样,您所要求的确切语法是不可能的,特别是因为您无法重新定义分隔符号)。 我的解决方案延伸了一些合理的做法,因为它在scala.Symbol上添加了一个操作符, 但如果您打算在受限范围内使用此DSL,那么这应该没问题。

object VarOps {
  val currentEqs = new util.DynamicVariable( Vector.empty[Eq] )
}
implicit class VarOps( val variable: Var ) extends AnyVal {
  import VarOps._
  def :=[T]( body: Var* ) = {
    val eq = Eq( variable, body:_* ) 
    currentEqs.value = currentEqs.value :+ eq
  }
}

implicit class SymbolOps( val sym: Symbol ) extends AnyVal {
  def apply[T]( body: => Unit ): Definition = { 
    import VarOps._
    currentEqs.withValue( Vector.empty[Eq] ) {
      body
      Definition( sym.name, currentEqs.value:_* )
    }
  }
}

现在你可以做到:

'Dummy {
   x := (y, z)
   y := (x, z)
}

构建以下定义(在REPL中打印):

Definition(Dummy,Vector(Eq(Var(x),WrappedArray(Var(y), Var(z))), Eq(Var(y),WrappedArray(Var(x), Var(z)))))