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.