6

Suppose that I have the following piece of Kotlin code:

fun main(args: Array<String>) {
    val a = "test"
    println(args.first())
}

If I pass in an argument $a, the output is going to be $a. As I understand it Kotlin takes care of String templates by generating the code for the output on compilation, presumably using a StringBuilder. Is there some way to evaluate Strings that aren't in the source code with regards to templates in their current context? String templates are very useful and it'd be great to be capable of evaluating expressions that come from a dynamic context, such as configuration files, but as far as I can tell this can't be done.

Lacking that, what would be a good approach towards this? Invoking a scripting engine?

G_H
  • 11,739
  • 3
  • 38
  • 82

3 Answers3

15

If you need to evaluate arbitrary expressions in this way, then yes, you need a scripting engine. Kotlin has a JSR 223 implementation that you can use, see the examples here (the kotlin-jsr223-* projects).

Here is a basic usage example:

val engine = ScriptEngineManager().getEngineByExtension("kts")!!
engine.eval("val x = 3")
val res = engine.eval("x + 2")
Assert.assertEquals(5, res)

The code is taken from KotlinJsr223ScriptEngineIT.kt, and remember to configure the service via META-INF

hotkey
  • 140,743
  • 39
  • 371
  • 326
  • A scripting engine for Kotlin itself sounds perfect. I wanted to avoid bringing JavaScript or JRuby into it when the the arguments operated on would be Kotlin ones. – G_H May 17 '17 at 03:40
  • By the way, I've just tried it for myself and found [a problem](https://youtrack.jetbrains.com/issue/KT-17921) with the 1.1.2 release of this tool, so if you encounter it too, then I'd recommend using 1.1.1 until it's fixed. – hotkey May 17 '17 at 10:58
  • 1.1.2 here, but after some experimenting I found that to access variables not declared in the script itself but passed in via bindings to the scripting engine, you have to us a construct `bindings["var_name"] as Type`. Is that intentional and will it stay that way? In Rhino you can just do something like `var_name + 2` and it evaluates. In the end the requirements I need this for are probably gonna necessitate writing a parser anyway, but it was interesting to try. – G_H May 17 '17 at 11:52
  • 1
    @G_H, I think you can work that around by binding a value first and then evaluating a code line that will declare it as a local variable in the script. Something like this: [(gist)](https://gist.github.com/h0tk3y/1966663456235e674c6971f5c1911db4) – hotkey May 17 '17 at 12:14
14

For completeness, here are also some ways to dynamically evaluate String templates in your Kotlin code.

The normal behavior of String templates is to evaluate the contents of the String at compile time. It will not change during the execution of your code:

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

    subject = "stackoverflow"
    println(hello)
}

// Output: hello world

There are multiple workarounds for this restriction:

Lambda function

You can wrap your String in a lambda function, so everytime you call your function, it will evaluate a new instance of the String.

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

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

// Output: hello stackoverflow

Advantage

  • Support String operations

Disadvantage

  • Must call it like a function

Anonymous object

You can create anonymous objects (similar to static in Java) and override the toString method.

fun main() {
    var subject = "world"
    val hello = object { override fun toString() = "hello $subject"}

    subject = "stackoverflow"
    println(hello())
}
    
// Output: hello stackoverflow

Note: I would not recommend this approach.

Advantage

  • No function call needed

Disadvantage

  • Does not support all String operations

Property Delegation

You can delegate property operator functions to an external class to achieve the dynamic evaluation of String templates.

import kotlin.reflect.KProperty

fun main() {
    var subject = "world"
    val hello: String by dynamicStringTemplate { "hello $subject" }

    subject = "stackoverflow"
    println(hello)
}

// Output: hello stackoverflow
 
class dynamicStringTemplate(var value: () -> String) {
    operator fun getValue(thisRef: Nothing?, prop: KProperty<*>): String {       
        return this.value()
    }
 
    operator fun setValue(thisRef:  Nothing?, prop: KProperty<*>, value: String) {
        this.value = {value}
    }
}

Advantages

  • Supports all String operations
  • Works the same as a String

Disadvantage

  • Additional class must be created
Michel Sahli
  • 1,059
  • 10
  • 14
2

Is there some way to evaluate Strings that aren't in the source code with regards to templates in their current context?

There are no template strings in your code sample and a is unused. Am I understanding correctly that you'd like to do something like val evaluated = evalStringTemplate(template, arg1, arg2, ...) with template being a String like "$a" and arg1, arg2, ... being arguments for the template?

If so, there's no Kotlin-specific way to do this, but you can use the Java Formatter class.

Christian Brüggemann
  • 2,152
  • 17
  • 23
  • Formatting Strings does just that, it formats. Kotlin templates can contain expressions, such as arithmetic operations, boolean logic, method calls etc. That's more what I'm after. – G_H May 17 '17 at 03:36