我想使用宏在Scala中实现外部DSL,例如SQL。我已经看过有关如何实施internal DSLs with Scala的论文。另外,我最近写了一篇关于how this can be done in Java的文章,我自己。
现在,内部DSL总是感觉有点笨拙,因为它们必须以宿主语言(例如Scala)实现和使用,并且遵守主机语言的语法限制。这就是为什么我希望Scala Macros能够在不受任何限制的情况下内化外部DSL。但是,我并不完全了解Scala Macros以及我可以与它们走多远。我已经看到SLICK以及一个名不见经传的名为sqltyped的库已开始使用宏,但SLICK使用“Scalaesque”语法进行查询,这不是SQL,而sqltyped使用用于解析SQL字符串的宏(也可以在没有宏的情况下完成)。此外,各种examples given on the Scala website对于我正在尝试做的事情来说太微不足道了
给出一个示例外部DSL定义为这样的一些BNF语法:
MyGrammar ::= (
'SOME-KEYWORD' 'OPTION'?
(
( 'CHOICE-1' 'ARG-1'+ )
| ( 'CHOICE-2' 'ARG-2' )
)
)
我可以使用Scala Macros实现上述语法,以允许这样的客户端程序吗?或者Scala Macros还不足以实现这样的DSL?
// This function would take a Scala compile-checked argument and produce an AST
// of some sort, that I can further process
def evaluate(args: MyGrammar): MyGrammarEvaluated = ...
// These expressions produce a valid result, as the argument is valid according
// to my grammar
val result1 = evaluate(SOME-KEYWORD CHOICE-1 ARG-1 ARG-1)
val result2 = evaluate(SOME-KEYWORD CHOICE-2 ARG-2)
val result3 = evaluate(SOME-KEYWORD OPTION CHOICE-1 ARG-1 ARG-1)
val result4 = evaluate(SOME-KEYWORD OPTION CHOICE-2 ARG-2)
// These expressions produce a compilation error, as the argument is invalid
// according to my grammar
val result5 = evaluate(SOME-KEYWORD CHOICE-1)
val result6 = evaluate(SOME-KEYWORD CHOICE-2 ARG-2 ARG-2)
注意,我对solutions that parse strings不感兴趣,sqltyped的方式
答案 0 :(得分:3)
自从这个问题得到了范式的回答以来已经有一段时间了,但是我偶然发现它并认为它值得延伸。
内化的DSL必须确实是有效的Scala代码,并且在宏扩展之前定义了所有名称,但是一个可以通过精心设计的语法和 Dynamics 克服此限制。 / p>
让我们说我们想创建一个简单,愚蠢的DSL,让我们以优雅的方式介绍人们。它可能看起来像这样:
people {
introduce John please
introduce Frank and Lilly please
}
我们希望将上述代码翻译(作为编译的一部分)到一个对象(例如从类People
派生的类)中,该对象包含每个引入的人类型Person
的字段的定义 - 像这样:
new People {
val john: Person = new Person("John")
val frank: Person = new Person("Frank")
val lilly: Person = new Person("Lilly")
}
为了实现这一点,我们需要定义一些具有两个目的的人工对象和类:定义语法(有点......)并欺骗编译器接受未定义的名称(如John
或Lilly
)
import scala.language.dynamics
trait AllowedAfterName
object and extends Dynamic with AllowedAfterName {
def applyDynamic(personName: String)(arg: AllowedAfterName): AllowedAfterName = this
}
object please extends AllowedAfterName
object introduce extends Dynamic {
def applyDynamic(personName: String)(arg: AllowedAfterName): and.type = and
}
这些虚拟定义使我们的DSL代码合法 - 编译器在继续进行宏扩展之前将其转换为以下代码:
people {
introduce.applyDynamic("John")(please)
introduce.applyDynamic("Frank")(and).applyDynamic("Lilly")(please)
}
我们是否需要这种丑陋且看似多余的please
?有人可能会提出一个更好的语法,例如使用Scala的后缀运算符表示法(language.postfixOps
),但由于分号推断而变得棘手(您可以在REPL控制台或IntelliJ&#中自己尝试) 39; s Scala工作表)。最简单的方法是使用未定义的名称交换关键字。
由于我们已经使语法合法化,我们可以使用宏处理块:
def people[A](block: A): People = macro Macros.impl[A]
class Macros(val c: whitebox.Context) {
import c.universe._
def impl[A](block: c.Tree) = {
val introductions = block.children
def getNames(t: c.Tree): List[String] = t match {
case q"applyDynamic($name)(and).$rest" =>
name :: getNames(q"$rest")
case q"applyDynamic($name)(please)" =>
List(name)
}
val names = introductions flatMap getNames
val defs = names map { n =>
val varName = TermName(n.toLowerCase())
q"val $varName: Person = new Person($n)"
}
c.Expr[People](q"new People { ..$defs }")
}
}
宏通过模式匹配查找所有引入的名称,以扩展动态调用,并生成所需的输出代码。请注意,宏必须是白盒,才能允许从签名中声明的类型返回派生类型的表达式。
答案 1 :(得分:2)
我不这么认为。传递给宏的表达式必须是有效的Scala表达式,并且应定义标识符。