5

我正在尝试在 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 可以实现我想要的吗?如果是的话,谁能告诉我一个简单的方法来实现它?

4

3 回答 3

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)))))
于 2013-01-26T14:56:34.233 回答
5

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

冒号

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

line1 ::
line2 ::
line3 ::
Nil

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

流体 API

另一个对 DSL 来说可能很有趣的替代方案是这样的:

BuildDefinition()
.line1
.line2
.line3
.build

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

构造函数 API

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

new BuildInterface {
  line1
  line2
  line3
}

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

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

于 2013-01-28T09:28:45.277 回答
1

这是另一种相对简单的解决方案,并且可以使用非常接近您理想的语法(正如其他人指出的那样,您要求的确切语法是不可能的,特别是因为您无法重新定义分隔符)。我的解决方案扩展了一些合理的做法,因为它scala.Symbol在 .

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)))))
于 2014-02-26T17:21:42.390 回答