Котлин ДСЛ - профсоюзная структура - PullRequest
0 голосов
/ 09 ноября 2018

Я проектирую DSL и сталкиваюсь с требованием, когда у меня есть переменная, которая может быть назначена по-разному. Сильно упрощая, я хотел бы установить свойство value либо целым числом, либо выражением в строке. (Реальная потребность еще сложнее.)

Я хотел бы написать в моем DSL:

value = 42

или

value = "6*7"

За сценой значение будет сохранено в структуре DynamicValue<Int>, которая содержит целое число или выражение.

class DynamicValue<T>(dv : T?, expr : String) {
    val directValue : T? = dv
    val script : String? = expr
    ...
}

Я пробовал несколько способов (делегат, класс и т. Д.), Но ни один из них не предоставлял такой синтаксис.

Есть ли способ объявить этот союз как структуру?

Ответы [ 2 ]

0 голосов
/ 10 ноября 2018

Ответ Рене дал мне преимущество, и, наконец, я пришел с этим решением.В этом решении я учел все мои требования (те, которые я пропустил в исходном вопросе), так что это стало намного сложнее, чем требовалось в моем первоначальном вопросе.значения или сценарии (фрагменты), работающие в хорошо защищенном контексте.Эти сценарии будут сохранены и выполнены позже.Я хотел включить всю мощь среды IDE при написании сценария, но хотел бы защитить свои сценарии от внедрения кода и помочь пользователю использовать только те значения контекста, которые требуются сценарию.

Уловка, которую я использовал для достижения этой цели, заключается в том, чтобы включить добавление сценария в kotlin, но перед тем, как запустить весь сценарий DSL и создать бизнес-объекты, я преобразую сценарий в строку.(Эта строка будет выполнена позже в защищенном, свернутом контексте с помощью механизма JSR233.) Этот разговор заставил меня токенизировать весь сценарий перед выполнением и найти / заменить некоторые из токенов.(Весь токенизатор и конвертер довольно длинный и скучный, поэтому я не буду здесь вставлять.)

Первый подход

Какова была моя цель - написать что-нибудь из этого:

myobject {
    value = static { 42 }                // A static solution
    value = static { 6 * 7 }             // Even this is possible
    value = dynamic{ calc(x, y) }        // A pure cotlin solution with IDE support
    value = dynamic("""calc(x * x)""")   // This is the form I convert the above script to
}

где calc, x и y определены в контекстном классе:

class SpecialScriptContext : ScriptContextBase() {
    val hello = "Hello"
    val x = 29
    val y = 13

    fun calc(x: Int, y: Int) = x + y

    fun greet(name: String) = println("$hello $name!")
}

Итак, давайте посмотрим на решение!Сначала мне нужен класс DynamicValue для хранения одного из значений:

class DynamicValue<T, C : ScriptContextBase, D: ScriptContextDescriptor<C>> 
    private constructor(val directValue: T?, val script: String?) {
    constructor(value: T?) : this(value, null)
    constructor(script: String) : this(null, script)
}

Эта структура гарантирует, что будет установлен только один из параметров (статический, скрипт).(Не беспокойтесь о параметрах типа C и D, они предназначены для поддержки контекстных сценариев.)

Затем я создал функции DSL верхнего уровня для поддержки синтаксиса:

@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> static(block: () -> T): DynamicValue<T, C, D>
        = DynamicValue<T, C, D>(value = block.invoke())

@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> dynamic(s: String): DynamicValue<T, C, D>
        = DynamicValue<T, C, D>(script = s)

@PlsDsl
fun <T, C : ScriptContextBase, D : ScriptContextDescriptor<C>> dynamic(block: C.() -> T): DynamicValue<T, C, D> {
    throw IllegalStateException("Can't use this format")
}

Anобъяснение к третьей форме.Как я уже писал ранее, я не хочу выполнять блок функции.Когда скрипт выполняется, эта форма преобразуется в строковую форму, поэтому обычно эта функция никогда не появляется в скрипте при выполнении.Исключением является предупреждение о нарушении работоспособности, которое никогда не будет выдано.

Наконец добавлено поле в построитель моих бизнес-объектов:

@PlsDsl
class MyObjectBuilder {
    var value: DynamicValue<Int, SpecialScriptContext, SpecialScriptContextDescriptor>? = null
}

Второй подход

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

Что помогло: функции расширения, инфиксные функции и закрытые классы.

Сначала я разделил два типа значений на отдельные классы, определяющие общего предка:

sealed class Value<T, C : ScriptContextBase> {
    abstract val scriptExecutor: ScriptExecutor
    abstract val descriptor: ScriptContextDescriptor<C>
    abstract val code: String
    abstract fun get(context: C): T?
}

class StaticValue<T, C : ScriptContextBase>(override val code: String,
                                                                 override val scriptExecutor: ScriptExecutor,
                                                                 override val descriptor: ScriptContextDescriptor<C>,
                                                                 val value: T? = null
) : Value<T, C>() {
    override fun get(context: C) = value

    constructor(oldValue: Value<T, C>, value: T?) : this(oldValue.code, oldValue.scriptExecutor, oldValue.descriptor, value)
}

class DynamicValue<T, C : ScriptContextBase>(override val code: String,
                                                                  script: String,
                                                                  override val scriptExecutor: ScriptExecutor,
                                                                  override val descriptor: ScriptContextDescriptor<C>)
    : Value<T, C>() {

    constructor(oldValue: Value<T, C>, script: String) : this(oldValue.code, script, oldValue.scriptExecutor, oldValue.descriptor)

    private val scriptCache = scriptExecutor.register(descriptor)
    val source = script?.replace("\\\"\\\"\\\"", "\"\"\"")
    private val compiledScript = scriptCache.register(generateUniqueId(code), source)

    override fun get(context: C): T? = compiledScript.execute<T?>(context)
}

Примечание, что я сделал основной конструктор внутренним и создал своего рода конструктор копирования и изменения.Затем я определил новые функции как расширение общего предка и пометил их как инфикс:

infix fun <T, C : ScriptContextBase> Value<T, C>.static(value: T?): Value<T, C> = StaticValue(this, value)

infix fun <T, C : ScriptContextBase> Value<T, C>.expr(script: String): Value<T, C> = DynamicValue(this, script)

infix fun <T, C : ScriptContextBase> Value<T, C>.dynamic(block: C.() -> T): Value<T, C> {
    throw IllegalStateException("Can't use this format")
}

Использование вторичного конструктора copy-and-alter позволяет наследовать контекстно-зависимые значения.Наконец, я инициализирую значение внутри построителя DSL:

@PlsDsl
class MyDslBuilder {
    var value: Value<Int, SpecialScriptContext> = StaticValue("pl.value", scriptExecutor, SpecialScriptContextDescriptor)
    var value2: Value<Int, SpecialScriptContext> = StaticValue("pl.value2", scriptExecutor, SpecialScriptContextDescriptor)
}

Все на месте, и теперь я могу использовать его в своем скрипте:

myobject {
    value static 42
    value2 expr "6 * 7"
    value2 dynamic { calc(x, y) }
}
0 голосов
/ 09 ноября 2018

Что вы думаете о следующем синтаксисе:

value(42)
value("6*7")
//or
value+=42
value+="6*7"

Вы можете сделать это с помощью функций оператора:

class DynamicValue<T>() {
    var dv: T? = null
    var expr: String? = null

    operator fun invoke(dv : T)  {
        this.dv = dv
        this.expr = null
    }

    operator fun invoke(expr: String)  {
        this.dv = null
        this.expr = expr
    }

    operator fun plusAssign(dv : T)  {
        this.dv = dv
        this.expr = null
    }

    operator fun plusAssign(expr: String)  {
        this.dv = null
        this.expr = expr
    }
}  

Вы не можете переопределить оператор присваивания в Kotlin, поэтому чистый синтаксис value=42 невозможен.

Но я бы не стал использовать операторские функции, это волшебство. Я бы сделал это:

val value = DynamicValue<Int>()
value.simple=42
value.expr="6*7"

class DynamicValue2<T>() {
    private var _dv: T? = null
    private var _expr: String? = null
    var simple: T?
        get() = _dv
        set(value) {
            _dv = value
            _expr = null
        }

    var expr: String?
        get() = _expr
        set(value) {
            _expr = value
            _dv = null
        }
}
...