Extension function whose name is stored in a variable

Hi all,

My goal is to create an extension function for a class for each key added to a Map. The function needs to have the value of the key. So, if I have such variable:

val function_name = “do_something”

Could it be possible to use the content of this variable to create an extension function (for exemple for the class String), allowing me then to write:

“My string”.do_something()

I was thinking about something like: fun String::function_name() { }

Thank you

Fabrice

In this way, no. Kotlin is a strongly typed language, so a string is a string, a function is a function.

The safe way to do this is to make a process function:

class MyClass {
    private fun doThis() {}
    private fun doThat() {}
    
    fun processDynamic(method: String) {
        when(method.toLowerCase()) {
            "this" -> doThis()
            "that" -> doThat()
            else -> throw IllegalArgumentException("I can't do '$method'")
        }
    }
}

This is a safe way to do it. No code injection allowed. But you need to add a new function manually to the switch case.

The other way is more dynamic, but needs more work and required reflection (you have to add kotlin-reflect to your dependencies).


@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class MyDynamic

class MyClassReflect {
    @MyDynamic
    fun doThis() { println("this")}

    @MyDynamic
    fun doThat() { println("that")}

    fun doCantBeCalled() {}

    @OptIn(ExperimentalStdlibApi::class)
    fun processDynamic(method: String) {
        val clazz = this::class
        val f = clazz.declaredFunctions.firstOrNull {
            it.name.toLowerCase() == "do${method.toLowerCase()}"
        } ?: throw IllegalArgumentException("I can't do '$method'")

        // It is always good idea to restrict access by annotation
        if (!f.hasAnnotation<MyDynamic>())
            throw IllegalArgumentException("Access denied for '$method'")

        f.call(this)
    }
}

fun main() {
    val c = MyClassReflect()
    c.processDynamic("this")
    c.processDynamic("that")
    c.processDynamic("cantBeCalled")
}

This will print:

this
that
Exception in thread "main" java.lang.IllegalArgumentException: I can't do 'cantBeCalled'

Note: using the marker annotation is not required, but a good idea to avoid users to call functions not intend to be called through string.
Also noteworthy, that this approach needs your functions to be public, therefore it may damage encapsulation. On the other hand, this solution let’s you dynamically extend the set of functions to be called by simply annotating it by the marker.

This reflenction-based solution I usually use with Reflections library to dynamically scan all implementations of an interface and instantiate them.

2 Likes

Thank you for your answer. Unfortunately i think it cannot be applied to my issue. Let me explain a little bit more my goal.

I would like to implement a DSL like that:

class DataBuilder {
    val data = mutableMapOf<String, Map<String,Double>>()

    fun build() {
        data.keys.forEach { key ->
            //how to extend DataBuilder with a function whose name is the same than the key value
        }
    }

}

fun addData(setup:DataBuilder.() -> Unit ):DataBuilder {
    val dataBuilder = DataBuilder()
    dataBuilder.setup()
    dataBuilder.build()
    return dataBuilder
}

And the user should be able to do that:

val builder = addData {
    data["dataset_1"] = mapOf(
        "measure_1" to 2.0,
        "measure_2" to 3.0
    )

    data["dataset_2"] = mapOf(
        "measure_1" to 12.0,
        "measure_2" to 34.0
    )
    
}

//here is the point, how to have a function whose name is the one chosen by the user for the dataset
builder.dataset_1()

Thanx!

hmmmm, you could maybe do builder["dataset_1"] instead? as in define a function

operator fun get(name: String): ReturnType {
    // Calculate some data
    return ...
}

and use it as specified

Yes. But my final goal is to be able to use this function with the DSL syntax (i simplified its usage in my previous post). Something like:

addData {
    data["dataset_1"] = mapOf(
        "measure_1" to 2.0,
        "measure_2" to 3.0
    )

    data["dataset_2"] = mapOf(
        "measure_1" to 12.0,
        "measure_2" to 34.0
    )

   select {
        //could it be possible that the DSL has dynamically added a property or function or class able to filter the first dataset by using its key value??
        dataset_1 < 3.0
   }

    
}

But it seems not possible to implement that.

1 Like

Please help me what this code should return?

select {
        //could it be possible that the DSL has dynamically added a property or function or class able to filter the first dataset by using its key value??
        dataset_1 < 3.0
   }

Does it return the submap of measurements where the value is lower than 3?

At least it filters the map linked to the key “dataset_1”. The idea is to have something more concise than:

select {
    name = "dataset_1"
    values < 3.0
}

Still not exactly see the big picture. What would be the output of the addData builder?

There are options. First, you can make it tidier:

typealias DataLabel = String

fun addData(op: DataBuilder.() -> Unit) = DataBuilder().apply(op).build()

class DataBuilder {
    val data = mutableMapOf<DataLabel, Map<String, Double>>()

    private var filtered: Map<String, Double> = mapOf()

    fun select(datasetId: DataLabel, predicate: (Double) -> Boolean) {
        if (data.containsKey(datasetId)) {
            filtered = data.getValue(datasetId).filterValues(predicate)
        } else throw java.lang.IllegalArgumentException("No dataset by name: $datasetId")
    }

    fun build() : Map<String, Double> {
        // Do whatever you need with filtered
        return filtered
    }
}

The type alias was added before I read your answer, it is not required, you can use String. With the above example, the usage:

addData {
        data["dataset_1"] = mapOf(
            "measure_1" to 2.0,
            "measure_2" to 3.0
        )

        data["dataset_2"] = mapOf(
            "measure_1" to 12.0,
            "measure_2" to 34.0
        )

        select("dataset_1") { it < 3.0 }
    }

However, if your select is internal to the addData, you may omit the local variable and simlify the DSL:

typealias DataLabel = String

fun addData(op: DataBuilder.() -> Unit) = DataBuilder().apply(op)

class DataBuilder {
    val data = mutableMapOf<DataLabel, Map<String, Double>>()

    fun select(datasetId: DataLabel, predicate: (Double) -> Boolean) : Map<String, Double> {
        if (data.containsKey(datasetId)) {
            return data.getValue(datasetId).filterValues(predicate)
        } else throw java.lang.IllegalArgumentException("No dataset by name: $datasetId")
    }
}

If you would like more precise solution (I’m just trying to figure out the “big picture”), please give me more information! What is the output of addData? It could be a builder, a wrapper or a method manipulating out-of-dsl objects. Also, what would you like to do with the filtered set? Is it used internally in the DSL or is it the result (return value) of the DSL?

You can also do this:

typealias DataLabel = String

fun addData(op: DataBuilder.() -> Unit) = DataBuilder().apply(op)

class DataBuilder {
    val data = mutableMapOf<DataLabel, Map<String, Double>>()

    infix fun DataLabel.select(predicate: (Double) -> Boolean) : Map<String, Double> {
        if (data.containsKey(this)) {
            return data.getValue(this).filterValues(predicate)
        } else throw java.lang.IllegalArgumentException("No dataset by name: $this")
    }
}

and use in this way:

addData {
        // ...
        val filtered = "dataset_1" select { it < 3.0 }
    }

but this looks messier to me than the previous option.

Hi,

thank you for all your answers. I will test them. Btw, concerning the full picture, this is the GitHub project related to my question: GitHub - fjossinet/RNArtistCore: A Kotlin DSL and library to create and plot RNA 2D structures

Best

3 Likes

Nice library and the DSL looks great, too. I am a great fun of the DSL feature, too. (See: PTT Kotlin )
Here are some additional ideas, you may use:

Using objects for constants

Using enums in DSL is useful, but pollutes it because you have to use ALL_CAPS, and prefix with enum class. However, using an object wrapper is easy and makes it clear:

enum class Options {
    ONE, TWO, THREE
}

sealed class OptionsWrapper( val value: Options )
object one : OptionsWrapper(Options.ONE)
object two : OptionsWrapper(Options.TWO)
object three : OptionsWrapper(Options.THREE)

// ...

class ColorBuilder {
    private val colors = mutableMapOf<Options, String>()

    infix fun OptionsWrapper.to(color : String) {
        colors[this.value] = color
    }

    // Just an example, any other operator would do
    operator fun OptionsWrapper.minus(color : String) {
        this to color
    }
}

Also, if you name all the node types (“U”, “C”, “G”, “A”), you can use the parent object everywhere you use string now. That would restrict input and one could not use “W” for example. Usage:

color {
        one to "#111111"
        two - "#111"
}

(Sure the to may be set or color and the minus is not too appropriate here. These are only for presetation.)