我正在尝试在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是我想要的吗?如果是的话,有人能告诉我一个实现它的简单方法吗?
答案 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
看起来有点难看。
另一种可能对DSL感兴趣的替代方案是:
BuildDefinition()
.line1
.line2
.line3
.build
其中每一行是构建器的成员函数(并返回修改后的构建器)。此解决方案需要最终将构建器转换为列表(可能作为隐式转换完成)。请注意,对于某些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)))))