1

I wonder if there is a way to create dynamic string templates. It is a string that would change its value when its parameters are changed.

val param = mutableListOf("a")
val paramString = "${param[0]}"
print(paramString)
param[0] = "b"
print(paramString)
// this prints `aa`, I want to get `ab`

A simple String.replace() wouldn't work, because the initial task is to evaluate the same long sql query for the list of given tables. In this sql query, there are other template parameters which might contain unpredicеable symbols that might be replaced by calling String.replace().

The best idea that I've got currently is to split the query into two strings start and end, and then execute get the query string like this:

val tables = listOf("employees", "customers") // tables for query
val start = "SELECT * FROM "
val end = """// some big query here
...
"""
for(table in tables) {
    val query = start + table + end
    // do something with query
}

But I think dynamic templates might be useful in more complex cases, that couldn't be solved that simply.

llesha
  • 423
  • 1
  • 15
  • Just write a function and call it repeatedly with different parameters. Or use property values as inputs if you like. – Tenfour04 May 08 '23 at 02:41

3 Answers3

3

All the answers that were given so far are 100% correct, but since you had the idea of just referencing a val and not call a function, I thought it might be a helpful addition to mention the possibility of property delegation. You could come up with a delegate like:

class StringTemplateDelegate(private val producer: () -> String) {
    private var fixedString: String? = null
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return fixedString ?: producer()
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        fixedString = value
    }
}

fun dynamicTemplate(producer: () -> String) = StringTemplateDelegate(producer)

and then use it like this:

val param = mutableListOf("a")
val paramString by dynamicTemplate { "${param[0]}" }
print(paramString) // prints 'a'
param[0] = "b"
print(paramString) // prints 'b'

here is a runnable example on the Kotlin Playground for you to try out or edit.

Remarks:

  1. Although I named the delegate 'StringTemplateDelegate' and the provider function 'dynamicTemplate', nothing about this implementation is specific for only Strings or even string templates. Swap out the String types for a generic, and you have a delegate that just executes a provider function whenever its value is accessed.

  2. I coded the delegate so that it can technically also be used as a var, and if you were to reassign it, it would just have that constant value you reassigned it to. It's a questionable functionality, but it demonstrates how delegates can also be used for vars.

Raphael Tarita
  • 770
  • 1
  • 6
  • 31
2

The idea is interesting, but this is not a language feature in any language I know. The critical question is how the program would decide whether the backing information has changed and therefore has to be recalculated, unless you want it to be calculated on each access.

If you want it to be updated per access, then what you seek is a function, because Strings are immutable, and therefore cannot be changed after creation. So unless you change the paramString reference, the value unterneath it will always remain the same. (You can cheat on this, but I would strongly discourage this, as it is will have unexpected side effects and not work reliably in every situation)

However, you could create a DynamicStringTemplate class which could do this, but again, it would have to be recalculated on each access. I once built something similar for JUnit tests, where I needed dynamically built Strings to compare against, but I used double questionmarks as placeholders and inserted the values dynamically from a list based on index. This is a bit less pretty that what you are looking for, but will do the job in most simple cases.

You could also do something like this, though it sadly does not come with the fancy template syntax you like:

class DynamicStringTemplate private constructor() {
    private val elements = ArrayList<Any>()
    override fun toString(): String = get()

    fun get(): String {
        val state = StringBuilder()
        for (element in elements) {
            if (element is Function0<*>) {
                state.append(element.invoke())
            } else {
                state.append(element)
            }
        }
        return state.toString()
    }

    fun append(element: Any): DynamicStringTemplate {
        elements += element
        return this
    }

    operator fun plus(element: Any): DynamicStringTemplate = append(element)
    operator fun invoke(): String = get()

    companion object {
        fun dynTemplate() = DynamicStringTemplate()
        fun dynTemplate(builder: () -> String): DynamicStringTemplate = dynTemplate(builder as Any)
        fun dynTemplate(vararg element: Any): DynamicStringTemplate {
            val template = DynamicStringTemplate()
            template.elements.addAll(element)
            return template
        }
    }
}

Here are some test cases to showcase how this works:

@Test
fun test_MonolithicLambdaBuilder() {
    val list = arrayListOf(1, "b", null)
    val template = dynTemplate { "My list contains $list." }
    assertEquals(template(), "My list contains [1, b, null].")
    list[2] = "42"
    assertEquals(template(), "My list contains [1, b, 42].")
}


// This might be the most interesting case for you...
@Test
fun test_NoValueCaptureInKotlin() {
    var a = "a"
    val template = dynTemplate { "My value is $a." }
    assertEquals(template(), "My value is a.")
    a = "b"
    assertEquals(template(), "My value is b.")
}

@Test
fun test_ChainBuilder() {
    val list = arrayListOf(1, "b", null)
    val template = dynTemplate("My list contains ") + list + "."
    assertEquals(template(), "My list contains [1, b, null].")
    list[2] = "42"
    assertEquals(template(), "My list contains [1, b, 42].")
}

@Test
fun test_VarargBuilder() {
    val list = arrayListOf(1, "b", null)
    val template = dynTemplate("My list contains ", list, ".")
    assertEquals(template(), "My list contains [1, b, null].")
    list[2] = "42"
    assertEquals(template(), "My list contains [1, b, 42].")
}

@Test
fun test_Callbacks() {
    var counter = 0
    val template = dynTemplate("This template was called ", { ++counter }, " times.")
    assertEquals(template(), "This template was called 1 times.")
    assertEquals(template(), "This template was called 2 times.")
    assertEquals(template(), "This template was called 3 times.")
}

@Test
fun test_MonolithicCallback() {
    var counter = 0
    val template = dynTemplate { "This template was called ${ ++counter } times." }
    assertEquals(template(), "This template was called 1 times.")
    assertEquals(template(), "This template was called 2 times.")
    assertEquals(template(), "This template was called 3 times.")
}

This has the added benefit, that you can manipulate the template object by passing it to other functions, like configuration evaluators.

TreffnonX
  • 2,924
  • 15
  • 23
  • That's probably overcomplicated for many uses, but is a neat idea, very powerful, and well implemented! – gidds May 08 '23 at 15:23
  • The idea was that this way, you can explicitly control the point in time when the callbacks are resolved. Or you can just pass the object, and `toString` will take care of the resolution at the point of string conversion. Otherwise the other answers are way more elegant. – TreffnonX May 09 '23 at 06:06
1

val paramString = "${param[0]}"

paramString is immutable variable, there is no way to change that.


Maybe, you can try the function way to do what you want.

Like this:

fun main() {
    var subject = "world"
    val hello = {"hello $subject"}
    println(hello())

    subject = "stackoverflow"
    println(hello())
}

/* Output: 
hello world
hello stackoverflow
*/

Hope that helps.

llesha
  • 423
  • 1
  • 15
Mason
  • 78
  • 8