Могу ли я использовать макросы Scala для интернализации внешнего DSL?

Я хотел бы реализовать внешний DSL, такой как SQL, в Scala с использованием макросов. Я уже видел документы о том, как реализовать внутренние DSL с помощью Scala. Кроме того, я недавно написал статью о как это можно сделать на 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 делает


person Lukas Eder    schedule 28.12.2012    source источник
comment
В настоящее время макросы ограничены вызовами обычных функций. То есть единственный способ запустить расширение макроса - это вызвать макрос def. Более того, аргументы макроса def должны быть хорошо типизированными выражениями Scala, что означает, что вы должны соответствовать синтаксису Scala и правилам ввода. Будущие исследования могут снять последнее или даже первое ограничение, но как и когда это произойдет, неясно.   -  person Eugene Burmako    schedule 28.12.2012
comment
@EugeneBurmako: Спасибо за авторитетный отзыв! Это можно расценивать как ответ, раз вы парень Scala Macros :-)   -  person Lukas Eder    schedule 28.12.2012
comment
@LukasEder: Вы говорите, что подход к синтаксическому анализу строк может быть реализован без макросов, но это правда только наполовину - не стоит недооценивать значение безопасности во время компиляции.   -  person Travis Brown    schedule 28.12.2012
comment
@TravisBrown: Да, я понимаю, что использование макросов для синтаксического анализа строк дает преимущества. Но тогда эти строки должны быть (встроенными) строковыми литералами, что заставляет меня чувствовать, что подход к синтаксическому анализу строк представляет собой кладж между истинными внутренними и истинными внешними DSL ... За исключением случаев, когда я что-то упускаю?   -  person Lukas Eder    schedule 28.12.2012
comment
Нет, вы правы - как общее решение DSL это беспорядочно, но во многих случаях (особенно там, где использование строковых литералов уже является обычной практикой - например, регулярные выражения, запросы SQL), возможность проверки встроенного языка во время компиляции является непростой задачей. огромный плюс.   -  person Travis Brown    schedule 28.12.2012
comment
@TravisBrown: Да, это правда. Хотя он работает только для статического регулярного выражения / SQL, а не для динамического регулярного выражения / SQL, я думаю ...   -  person Lukas Eder    schedule 28.12.2012


Ответы (2)


Прошло некоторое время с тех пор, как на этот вопрос был дан парадигматический ответ, но я только что наткнулся на него и подумал, что его стоит расширить.

Внутренний DSL действительно должен быть действительным кодом Scala со всеми именами, определенными до раскрытия макроса, однако можно преодолеть это ограничение с помощью тщательно разработанного синтаксиса и Dynamics.

Допустим, мы хотели создать простой и глупый 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 или в Scala Worksheet IntelliJ). Проще всего просто чередовать ключевые слова с неопределенными именами.

Поскольку мы сделали синтаксис допустимым, мы можем обработать блок с помощью макроса:

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

Макрос находит все введенные имена путем сопоставления шаблонов с расширенными динамическими вызовами и генерирует желаемый выходной код. Обратите внимание, что макрос должен быть белым ящиком, чтобы иметь возможность возвращать выражение типа, производного от того, которое было объявлено в подписи.

person Krotton    schedule 26.03.2015
comment
Это очень интересно! Большое спасибо за ответ на этот старый вопрос ... Я рассмотрю его позже, но это похоже на то, что я изначально просил - person Lukas Eder; 26.03.2015

Я так не думаю. Выражение, которое вы передаете макросу, должно быть допустимым выражением Scala, и должны быть определены идентификаторы.

person paradigmatic    schedule 28.12.2012
comment
Спасибо за ответ. Об этом же сказал Евгений Бурмако в своем комментарии. - person Lukas Eder; 28.12.2012
comment
Могут ли макросы Lisp анализировать синтаксис, отличный от Lispy? - person CMCDragonkai; 10.10.2014