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

我可以使用Scala宏内部化外部DSL吗?,scala,dsl,scala-macros,Scala,Dsl,Scala Macros,我想使用宏在Scala中实现一个外部DSL,比如SQL。我已经看过关于如何实施的文件。另外,我最近写了一篇关于我自己的文章 现在,内部DSL总是感觉有点笨拙,因为它们必须在宿主语言(如Scala)中实现和使用,并且遵守宿主语言的语法约束。这就是为什么我希望Scala宏能够允许内部化外部DSL,而无需任何此类约束。然而,我并不完全理解Scala宏,以及我可以使用它们走多远。我已经看到了这一点,还有一个鲜为人知的库,名为have,它已经开始使用宏,但是SLICK使用“scalasque”语法进行查询

我想使用宏在Scala中实现一个外部DSL,比如SQL。我已经看过关于如何实施的文件。另外,我最近写了一篇关于我自己的文章

现在,内部DSL总是感觉有点笨拙,因为它们必须在宿主语言(如Scala)中实现和使用,并且遵守宿主语言的语法约束。这就是为什么我希望Scala宏能够允许内部化外部DSL,而无需任何此类约束。然而,我并不完全理解Scala宏,以及我可以使用它们走多远。我已经看到了这一点,还有一个鲜为人知的库,名为have,它已经开始使用宏,但是SLICK使用“scalasque”语法进行查询,这不是真正的SQL,而sqltyped使用宏来解析SQL字符串(也可以不用宏来完成)。而且,对于我正在尝试做的事情来说,各种各样的都太琐碎了

我的问题是: 给出一个外部DSL示例,定义为如下所示的一些BNF语法:

MyGrammar ::= ( 
  'SOME-KEYWORD' 'OPTION'?
    (
      ( 'CHOICE-1' 'ARG-1'+ )
    | ( 'CHOICE-2' 'ARG-2'  )
    )
)
people {
  introduce John please
  introduce Frank and Lilly please
}
new People {
    val john: Person = new Person("John")
    val frank: Person = new Person("Frank")
    val lilly: Person = new Person("Lilly")
}
我是否可以使用Scala宏实现上述语法,以允许这样的客户端程序?还是Scala宏的功能不足以实现这样的DSL

//此函数将接受Scala compile checked参数并生成AST
//某种程度上,我可以进一步处理
def evaluate(args:MyGrammar):MyGrammarEvaluated=。。。
//这些表达式产生有效的结果,因为参数根据
//我的语法
val result1=评估(某些关键字选择-1 ARG-1 ARG-1)
val result2=评估(SOME-KEYWORD-CHOICE-2 ARG-2)
val result3=评估(某些关键字选项选择-1 ARG-1 ARG-1)
val result4=评估(SOME-KEYWORD选项选择-2 ARG-2)
//这些表达式产生编译错误,因为参数无效
//根据我的语法
val result5=评估(某些关键字选择-1)
val result6=评估(某些关键字选择-2 ARG-2 ARG-2)

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

这个问题已经有一段时间没有被paradigmatic回答了,但我刚刚偶然发现了它,认为它值得扩展

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

比方说,我们想创建一个简单、愚蠢的DSL,让我们能够以优雅的方式介绍人。可能是这样的:

MyGrammar ::= ( 
  'SOME-KEYWORD' 'OPTION'?
    (
      ( 'CHOICE-1' 'ARG-1'+ )
    | ( 'CHOICE-2' 'ARG-2'  )
    )
)
people {
  introduce John please
  introduce Frank and Lilly please
}
new People {
    val john: Person = new Person("John")
    val frank: Person = new Person("Frank")
    val lilly: Person = new Person("Lilly")
}
我们希望(作为编译的一部分)将上述代码翻译成一个对象(例如从class
People
派生的类),该对象包含每个介绍的人的
Person
类型字段的定义,如下所示:

MyGrammar ::= ( 
  'SOME-KEYWORD' 'OPTION'?
    (
      ( 'CHOICE-1' 'ARG-1'+ )
    | ( 'CHOICE-2' 'ARG-2'  )
    )
)
people {
  introduce John please
  introduce Frank and Lilly please
}
new People {
    val john: Person = new Person("John")
    val frank: Person = new Person("Frank")
    val lilly: Person = new Person("Lilly")
}
为了使之成为可能,我们需要定义一些有两个目的的人工对象和类:定义语法(有些…)和欺骗编译器接受未定义的名称(如
John
Lilly

这些伪定义使我们的DSL代码合法-编译器在继续宏扩展之前将其翻译为以下代码:

people {
    introduce.applyDynamic("John")(please)
    introduce.applyDynamic("Frank")(and).applyDynamic("Lilly")(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 }")
  }
}

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

当前宏仅限于普通函数调用。也就是说,触发宏展开的唯一方法是调用def宏。此外,def宏的参数必须是类型良好的Scala表达式,这意味着您必须遵守Scala的语法和类型规则。未来的研究可能会取消后者,甚至取消前者的限制,但如何以及何时发生尚不清楚。@EugeneBurmako:谢谢权威反馈!这可以作为一个答案,因为你是Scala宏的家伙:-)@LukasEder:你说字符串解析方法“可以不用宏来完成”,但这只对了一半,不要低估编译时安全性的价值。@TravisBrown:是的,我知道使用宏进行字符串解析有好处。但是,这些字符串需要是(内联的)字符串文本,这让我觉得字符串解析方法是介于真正的内部DSL和真正的外部DSL之间的“混球”。。。除非我遗漏了什么?不,你是对的,作为一个通用DSL解决方案,这是很困难的,但是在许多情况下(特别是在使用字符串文本已经很常见的情况下,例如正则表达式、SQL查询),能够在编译时验证嵌入式语言是一个巨大的优势。谢谢你的回答。这也是Eugene Burmako在他的评论中所说的Lisp宏可以解析非lispy语法吗?这很有趣!非常感谢您继续回答这个老问题。。。稍后我会仔细看一看,但这看起来和我最初要求的很接近