Kotlin

kotlin内でkotlin scriptを実行する

kotlin 1.1 でjavax.script API(JSR-223)がサポートされています。
https://kotlinlang.org/docs/reference/whatsnew11.html#javaxscript-support
これにより、kotlin上でkotlin(script)ソースをコンパイルして実行することができます。

公式にサンプルソースがありますので参照ください。
https://github.com/JetBrains/kotlin/tree/master/libraries/examples/kotlin-jsr223-local-example

以下、おおまかに説明します。
下記内容は kotlin 1.2.10 で確認しています。

目次

  1. 準備
  2. 実装
    2.1 単純な実行
    2.2 変数を渡す
    2.3 コンパイル
    2.4 eval
  3. 応用例:簡易テンプレートエンジン

1. 準備

Gradleプロジェクトを前提とします。
通常のkotlinプロジェクトにkotlin-script-utilパッケージを依存に加えます。

buildscript {
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.10"
    }
}

apply plugin: 'kotlin'

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.jetbrains.kotlin:kotlin-stdlib-jre8'
    compile 'org.jetbrains.kotlin:kotlin-script-util' // <--- 追加
}

compileKotlin {
    kotlinOptions.jvmTarget = '1.8'
}

SPI(Service Provider Interface)という仕組みを採用しています。src/main/resources/META-INF/services/javax.script.ScriptEngineFactory というファイルを作成し、以下の1行を書きます。
これでFactoryを見つけることができます。

org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

2. 実装

2.1 単純な実行

まずは簡単な例を実行します。

import javax.script.ScriptEngineManager

fun main(args: Array<String>) {
    val engine = ScriptEngineManager().getEngineByExtension("kts")
    engine.eval("println(\"Hello Kotlin\")") // Hello Kotlin
}

ScriptEngineManager().getEngineByExtension("kts")ScriptEngineを取得します。
ScriptEngineManager().getEngineByName("kotlin")でも取得できます。)
実態はorg.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineになります。
ScriptEngine#eval()でkotlinコードを評価します。

さらに、eval()は評価した結果を受け取ることができます。

fun main(args: Array<String>) {
    val engine = ScriptEngineManager().getEngineByExtension("kts")
    val route2 = engine.eval("kotlin.math.sqrt(2.0)") // kotlin.mathは1.2以上
    println(route2) // 1.4142135623730951
}

2.2 変数を渡す

スクリプトに変数を渡すことができます。
(ただし、あまり使い勝手が良くありません。)

fun main(args: Array<String>) {
    val engine = ScriptEngineManager().getEngineByExtension("kts")
    val bindings = engine.createBindings()
    bindings["x"] = "Hello!"
    engine.eval("""
        val xxx = bindings["x"]
        println(xxx)
        """, bindings) // Hello!
}

engine.createBindings()Bindingsインスタンスを取得し、MutableMap<String, Any?>のように値を入れます。
eval()の第2引数にBindingsインスタンスを指定し、スクリプトのなかでbindingsという変数で受け取ります。
スクリプトの中のbindingsという変数名は固定で、Map<String, Any?>型になっており、bindings["変数名"]で取得できる値はAny?型になるため、適切な型にキャストする必要があります。

fun main(args: Array<String>) {
    val engine = ScriptEngineManager().getEngineByExtension("kts")
    val bindings = engine.createBindings()
    bindings["x"] = "Hello!"
    engine.eval("""
        val xxx = bindings["x"] as String
        println(xxx.substring(1)) // as Stringがないとエラーになる
        """, bindings) // Hello!
}

2.3 コンパイル

上記の例ではeval()でコンパイルと実行を同時に行なっていましたが、事前にコンパイルだけ行い、コンパイルした結果を再利用することができます。

import javax.script.Compilable

fun main(args: Array<String>) {
    val engine = ScriptEngineManager().getEngineByExtension("kts")
    val compiled = (engine as Compilable).compile("""
        val xxx = bindings["x"]
        println(xxx)
        """)

    val bindings = engine.createBindings()
    bindings["x"] = "Hello!"
    compiled.eval(bindings)
}

engineCompilableにキャストしてcompile()でスクリプトをコンパイルしてCompiledScriptインスタンスを生成します。
その後、eval()で評価します。
スクリプトを再利用する場合はこのCompiledScriptインスタンスをキャッシュしておくと良いでしょう。

2.4 eval

スクリプトの中でスクリプトを組み立てて評価するeval()というメソッドが使えます。
あまりいい例が思いつきませんでしたが、公式も同じような例でした。

fun main(args: Array<String>) {
    val engine = ScriptEngineManager().getEngineByExtension("kts")
    engine.eval("""
        val x = 1
        val y = eval("${'$'}x + 1")
        println(y) // 2
    """)
}

3. 応用例:簡易テンプレートエンジン

以下のように、この技術を使って簡易テンプレートエンジンを作ることができます。
https://github.com/sdeleuze/kotlin-script-templating

要は最終的に文字列を返すスクリプトを用意し、それを評価すれば完成です。
ロジックは${}で記述します。式を返す必要があるので注意が必要です。
変数はbindingsで受け取るため、変数宣言が必要になります。

template.kts
import jsr223example.*

val name = bindings["name"] as String
@Suppress("UNCHECKED_CAST")
val users = bindings["users"] as List<User>
val condition = true

// ここからテンプレート
"""
Hello! ${name}

${if (condition) {
"""
Hello!
"""
} else ""}

${users.map {
"""
Hello! ${it.name}
"""}.joinToString("\n")}
""" // ここまで
  • if文はelse句がないと値を返さないためエラーになるのでelseが必須です。
  • for文は値を返さないので使用できません。その代わり、mapjoinToStingなどを使います。
  • bindingsはスクリプト上では未定義の変数なので、Intellijのエディタ上ではエラーになります。それを回避するため、別ファイルで変数をlateinit宣言してimportすることで見た目上はエラーになりません。
ScriptUtil.kt
package jsr223example

// Hack for Intellij
lateinit var bindings: Map<String, Any?>

実用で使用するのは難しいかもしれませんが、ちょっとしたツールなどには使えるかもしれません。

以上になります。

上記のソースは
https://github.com/tiibun/jsr223example
に置いています。