Scala — проектирование DSL с минимальным синтаксисом

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

outer {
    inner(id = "asdf") {
        value("v1")
        value("v2")
    }
}

Этот фрагмент должен давать следующее значение:

Outer(Inner("asdf", Value("v1") :: Value("v2") :: Nil))

Данные структуры данных

case class Outer(inner: Inner)
case class Inner(values: List[Value])
case class Value(value: String)

Идея состоит в том, что функция inner доступна только в закрытии, следующем за outer, функция value доступна только в закрытии после inner и т. д. То есть следующее не будет компилироваться: outer { value("1") }.

Как я могу реализовать что-то подобное? В конце концов, структуры данных не обязательно должны быть неизменяемыми, они могут быть любыми, если они строго типизированы.

Я не знаком с макросами Scala, но могу ли я случайно решить эту проблему с помощью макросов?


Самое близкое, что у меня есть до сих пор, это следующая реализация:

object DSL extends App {

    def outer = new Outer()

    class Outer(val values: mutable.MutableList[Inner] = mutable.MutableList.empty) {
        def inner(id: String): Inner = {
            val inner = new Inner(id)
            values += inner
            inner
        }
        def apply(func: Outer => Unit): Outer = {
            func(this)
            this
        }
        override def toString: String = s"Outer [${values.mkString(", ")}]"
    }

    class Inner(val id: String, val values: mutable.MutableList[Value] = mutable.MutableList.empty) {
        def value(v: String): Value = {
            val value = new Value(v)
            values += value
            value
        }
        def apply(func: Inner => Unit): Unit = func(this)

        override def toString: String = s"Inner (${values.mkString(", ")})"
    }

    class Value(val str: String) {
        override def toString: String = s"Value<$str>"
    }

    val value = outer { o =>
        o.inner(id = "some_id") { i =>
            i.value("value1")
            i.value("value2")
        }
    }

    println(value)

Как я могу избавиться от аннотаций анонимных функций (например, o => и o. и т. д.)?

В качестве альтернативы есть способ рассматривать outer как new Outer (в этом случае следующий блок кода будет рассматриваться как конструктор, и я смогу вызывать функции-члены)?


person ak.    schedule 27.03.2015    source источник


Ответы (1)


Как вы заметили, это сводится к

есть ли способ рассматривать outer как new Outer

и к сожалению ответ нет. Думаю, это было возможно в экспериментальном форке Scala-Virtualized. Лично я думаю, что ключевое слово new тоже очень раздражает в Scala.


Я вижу только два решения.

  • используйте макросы или плагин компилятора
  • использовать глобальный изменяемый объект построителя

Я знаю два проекта, которые могли бы сделать эту работу за вас с первым подходом:

Я пробовал с первым. Я клонировал репозиторий и изменил scalaVersion в project/build.scala на "2.11.6" (вместо снапшота). Вы можете зайти в REPL, используя sbt sandbox/console.

Идея состоит в том, чтобы определить функции, которые принимают параметр, помеченный @Implicit, и таким образом вы можете «склеить» внешнюю и внутреннюю части дерева DSL вместе:

import org.dslparadise.annotations._
import scala.collection.mutable.Builder

case class Outer(inner: Inner)
case class Inner(id: String, values: List[Value])
case class Value(value: String)

def outer(i: Inner) = Outer(i)  // nothing special here

def inner(id: String)
         (body: (Builder[Value, List[Value]] @Implicit) => Unit): Inner = {
  val b = List.newBuilder[Value]  // to "build" the contents of inner
  body(b)
  Inner(id, b.result)
}

def value(x: String)(implicit b: Builder[Value, List[Value]]): Value = {
  val v = Value(x)
  b += v   // side-effect: populate the builder
  v
}

Пример:

scala> outer {
     |   inner(id = "asdf") {
     |     value("v1")
     |     value("v2")
     |   }
     | }
res1: Outer = Outer(Inner(asdf,List(Value(v1), Value(v2))))

Вуаля!


Решением без плагинов/макросов будет установка, например, ThreadLocal сборщиков, но тогда у вас не будет безопасности во время компиляции:

val values = new ThreadLocal[Builder[Value, List[Value]]]

def inner(id: String)(body: => Unit): Inner = { 
  val prev = values.get()
  values.set(List.newBuilder[Value])
  body
  val v = values.get().result
  values.set(prev)
  Inner(id, v)
}

def value(x: String): Value = { 
  val v = Value(x)
  values.get() += v
  v
}

Пример:

scala> inner(id = "asdf") { value("v1"); value("v2") }
res1: Inner = Inner(asdf,List(Value(v1), Value(v2)))
person 0__    schedule 28.03.2015
comment
Это блестяще. Я не задавал этот вопрос, но я рад, что наткнулся на этот ответ. Я пытался решить проблему, но мне никогда не приходило в голову, как легко это может быть с Builder и dsl-paradise! Спасибо! - person Chetan Bhasin; 28.03.2015