0

I am designing a DSL and run into a requirement where I have a variable which could be assigned to different ways. Greatly simplified, I would like to set value property either by an integer or by an expression in String. (The real need is even more complex.)

I would like to write in my DSL:

value = 42

or

value = "6*7"

Behind the scene, the value will be stored in a DynamicValue<Int> structure which contains either an integer or the expression.

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

I tried several ways (delegate, class, etc), but none of them provided these syntax.

Is there a way to declare this union like structure?

Balage1551
  • 1,037
  • 1
  • 10
  • 28

2 Answers2

1

What do you think about the following syntax:

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

You can do this with operator functions:

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

You can't redefine the assign operator in Kotlin, therefor the pure syntax value=42 is not possible.

But I wouldn't go with operator functions, it's to magical. I would do this:

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
        }
}
Rene
  • 5,730
  • 17
  • 20
0

Rene's answer gave me the lead and finally I turned up with this solution. In this solution I took all my requirements in (the ones I dropped out in my original question) so this became much more complicated than my original question would have required.

My whole requirement was to be able to add static values or scripts (snippets) running on a well guarded context. These script would be stored, and executed later. I wanted to enable the whole power of the IDE when writing the script, but would like to guard my scripts from code injections and help the user to use only the context values the script requires.

The trick I used to achieve this is to enable adding script in kotlin, but before I run the whole DSL script and create the business objects, I convert the script into a string. (This string will be executed later in a guarded, wrapped context by JSR233 engine.) This conversation forced me to tokenize the whole script before execution and search/replace some of the tokens. (The whole tokenizer and converter is rather long and boring, so I won't insert here.)

First approach

What my goal was to be able to write any of this:

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
}

where calc, x and y are defined in the context class:

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

So let's see the solution! First I need a DynamicValue class to hold one of the values:

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

This structure will ensure that exactly one of the options (static, script) will be set. (Don't bother with the C and D type parameters, they are for context-based script support.)

Then I made top level DSL functions to support syntax:

@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 explanation to the third form. As I wrote before, I don't want to execute the block of the function. When the script is executed, this form is converted to the string form, so normally this function would never appear in the script when executed. The exception is a sanity warning, which would never be thrown.

Finally added the field to my business object builder:

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

Second approach

The previous solution worked but had some flaws: the expression was not associated with the variable it set, neither with the entity the value was set in. With my second approach I solved this problem and removed the need of equal sign and most of the unnecessary curly brackets.

What helped: extension functions, infix functions and sealed classes.

First, I split the two value types into separated classes defined a common ancestor:

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

Note, that I made the primary constructor internal and created a kind of copy and alter constructor. Then I defined the new functions as extension of the common ancestor and marked them infix:

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

Using the secondary copy-and-alter constructor allows to inherit the context sensitive values. Finally I initialize the value inside the DSL builder:

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

Everything is in place and now I can use it in my script:

myobject {
    value static 42
    value2 expr "6 * 7"
    value2 dynamic { calc(x, y) }
}
Balage1551
  • 1,037
  • 1
  • 10
  • 28