我可以使用Scala Macros内部化外部DSL吗?

时间:2012-12-28 12:54:26

标签: scala dsl scala-macros

我想使用宏在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的方式

2 个答案:

答案 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")
}

为了实现这一点,我们需要定义一些具有两个目的的人工对象和类:定义语法(有点......)并欺骗编译器接受未定义的名称(如JohnLilly

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表达式,并且应定义标识符。