Я хочу разработать 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 (в этом случае следующий блок кода будет рассматриваться как конструктор, и я смогу вызывать функции-члены)?