3

我想使用宏在 Scala 中实现外部 DSL,例如 SQL。我已经看过有关如何使用 Scala实现内部 DSL的论文。另外,我最近自己写了一篇关于如何在 Java 中做到这一点的文章。

现在,内部 DSL 总是感觉有点笨拙,因为它们必须在宿主语言(例如 Scala)中实现和使用,并遵守宿主语言的语法约束。这就是为什么我希望 Scala 宏将允许在没有任何此类限制的情况下内部化外部 DSL。但是,我并不完全了解 Scala 宏以及我能用它们走多远。我已经看到SLICK和一个鲜为人知的名为sqltyped的库已经开始使用宏,但是 SLICK 使用“Scalaesque”语法进行查询,这不是真正的 SQL,而 sqltyped 使用宏来解析 SQL 字符串(可以也可以在没有宏的情况下完成)。此外,Scala 网站上给出的各种示例对于我正在尝试做的事情来说太微不足道了

我的问题是:

给定一个外部 DSL 示例,定义为一些 BNF 语法,如下所示:

MyGrammar ::= ( 
  'SOME-KEYWORD' 'OPTION'?
    (
      ( 'CHOICE-1' 'ARG-1'+ )
    | ( 'CHOICE-2' 'ARG-2'  )
    )
)

我可以使用 Scala 宏实现上述语法以允许这样的客户端程序吗?还是 Scala 宏不够强大,无法实现这样的 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)

注意,我对解析字符串的解决方案不感兴趣,就像sqltyped那样

4

2 回答 2

3

范式回答这个问题已经有一段时间了,但我只是偶然发现它并认为它值得扩展。

内部化的 DSL 确实必须是有效的 Scala 代码,其所有名称都在宏扩展之前定义,但是可以通过精心设计的语法和Dynamics克服这一限制。

假设我们想创建一个简单、愚蠢的 DSL,允许我们以优雅的方式介绍人们。它可能看起来像这样:

people {
  introduce John please
  introduce Frank and Lilly please
}

我们想(作为编译的一部分)将上述代码翻译成一个对象(例如从 class 派生的类),其中包含每个被介绍人People的类型字段定义- 如下所示:Person

new People {
    val john: Person = new Person("John")
    val frank: Person = new Person("Frank")
    val lilly: Person = new Person("Lilly")
}

为了使它成为可能,我们需要定义一些具有两个目的的人造对象和类:定义语法(有点……)和欺骗编译器接受未定义的名称(如Johnor 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 的 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 }")
  }
}

该宏通过与扩展的动态调用进行模式匹配来查找所有引入的名称,并生成所需的输出代码。请注意,宏必须是白盒才能允许返回从签名中声明的类型派生的类型的表达式。

于 2015-03-26T00:31:12.070 回答
2

我不这么认为。传递给宏的表达式必须是有效的 Scala 表达式,并且应该定义标识符。

于 2012-12-28T13:05:53.790 回答