【问题标题】:Can I use Scala Macros to internalise an external DSL?我可以使用 Scala 宏来内部化外部 DSL 吗?
【发布时间】:2023-04-04 22:31:01
【问题描述】:

我想使用宏在 Scala 中实现外部 DSL,例如 SQL。我已经看过有关如何实现internal DSLs with Scala 的论文。另外,我最近写了一篇关于how this can be done in Java的文章。

现在,内部 DSL 总是让人觉得有点笨拙,因为它们必须在宿主语言(例如 Scala)中实现和使用,并遵守宿主语言的语法约束。这就是为什么我希望 Scala 宏将允许在没有任何此类限制的情况下内部化外部 DSL。但是,我并不完全了解 Scala 宏以及我能用它们走多远。我已经看到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 宏实现上述语法以允许这样的客户端程序吗?还是 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)

注意,我对 solutions that parse strings 不感兴趣,就像 sqltyped 一样

【问题讨论】:

  • 目前宏仅限于普通函数调用。也就是说,触发宏扩展的唯一方法是调用 def 宏。此外,def 宏的参数必须是类型良好的 Scala 表达式,这意味着您必须符合 Scala 的语法和类型规则。未来的研究可能会解除后者甚至前者的限制,但如何以及何时发生尚不清楚。
  • @EugeneBurmako:感谢权威反馈!这可以作为一个答案,因为你是 Scala 宏的人:-)
  • @LukasEder:你说字符串解析方法“可以在没有宏的情况下完成”,但这只是对了一半——不要在这里低估编译时安全的价值。
  • @TravisBrown:是的,我知道使用宏进行字符串解析有好处。但是,这些字符串将需要是(内联)字符串文字,这让我觉得字符串解析方法是真正内部和真正外部 DSL 之间的“杂物”......除非我错过了什么?
  • 不,你是对的——作为一个通用的 DSL 解决方案,它很笨拙,但在许多情况下(特别是在使用字符串文字已经很普遍的情况下——例如,正则表达式、SQL 查询)能够验证编译时的嵌入式语言是一个巨大的优势。

标签: scala dsl scala-macros


【解决方案1】:

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

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

假设我们想创建一个简单、愚蠢的 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 的 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 }")
  }
}

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

【讨论】:

  • 这很有趣!非常感谢您跟进这个老问题...稍后我会仔细研究,但这看起来与我最初要求的很接近
【解决方案2】:

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

【讨论】:

  • 感谢您的回答。这也是 Eugene Burmako 在评论中所说的
  • Lisp 宏可以解析非 lispy 语法吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多