Kotlin Scripting in 2024

Written by: Jonathan Augustine

#!/usr/bin/env kotlin

A Little Preamble

Java was the my first real programming language. Learning it was my own cognitive Cambrian Explosion; I was able to create every idea that popped into my head: Discord bots, population genetics emulation, really (really) bad games, et cetera. It was cumbersome at first, but after you’ve used a tool for some time you stop noticing its faults. Accustomed to psvm and verbose Class definitions, I was satisfied with Java. Until I stumbled across the Kotlin 1.2 announcement and the promise of single-language, multiplatform programming.

Delving deeper into this Kotlin only made my excitement grow. Single-line class definitions? First-class functions? A whole new world of functional programming opened my eyes to just how heavy Java is. I almost immediately started transitioning my existing Java projects into Kotlin, typing away with glee as I watched the line-count shrink. I suddenly had a favorite programming language.

Kotlin’s Entry Cost

Despite Kotlin’s improved readability and transpiling magic, it still suffers from the JVM hero that is Gradle. It’s an amazing tool and has earned it’s place as Kotlin’s primary build tool, but no-one has ever accused Gradle of being easy to get into. In a world of package.json and cargo.toml, gradle.build and gradle.settings feel like extra roadblocks in the way of actually starting a new project. Just scrolling through r/Kotlin shows how commonly build.gradle.kts befuddles new programmers. Many tend to brush this off but they forget how long it took them to get accustomed to Gradle’s intricacies. Without IntelliJ’s project creation tools, the tedious boilerplate would certainly cause many newcomers to try another language.

The Potential of Kotlin Scripting

“What if Kotlin could have as little setup as TypeScript” is a thought that’s crossed all our minds at least once. As it turns out, there is (kind-of) a way out of Gradle: scripting! At the time of writing, there are two main solutions for scripting with Kotlin: the independent “KScript” and JetBrains' own Kotlin scripting. In this and following articles we'll explore both solutions to find which one can match the low inertia seen in TypeScript and Python.

The Plan

We’ll start by making a simple REST backend with both scripting implementations; the goal is to see how easy it is to:

1. Get from no project to a running server

2. Create and develop the project without IntelliJ

3. Package and execute the project

A Little Setup

Let’s use VSCode and Ubuntu (WSL2) as a neutral, non-IntelliJ environment. Make sure your installed Kotlin version is >=v1.4:

$ kotlin -version 
Kotlin version 1.9.23-release-779 (JRE 21.0.2)

Installing the Kotlin VSCode extension is a good idea if we still want some code-completion. Note that the extension won’t recognize .kts files by default for whatever reason, so make sure to change that in settings – even then it is far from perfect.

VSCode Kotlin Settings

JetBrains' Kotlin Scripting

The official “experimental” implementation from JetBrains is pretty much plug-and-play, but you might hit an apparent roadblock when exploring the documentation. The official guide for “Kotlin Custom Scripting” (KCS) would lead you to believe there’s much more bloat than necessary. Needing to write a “Script Definition” and “Scripting Host” across multiple files and directories before even touching fun main is an easy way to turn Kotlin scripting into another Gradle lesson; luckily, KCS isn’t what we’re looking for today. We can easily write Kotlin code with maven dependencies and file imports and barely learn any new syntax!

Let’s make a new directory and our entry file:

$ mkdir kotlin-scripting && touch ./kotlin-scripting/entry.main.kts

Open entry.main.kts and add a shebang to the top of the file along with a simple print statement:

#!/usr/bin/env kotlin 

println("Oh Hi Mark")

This will allow us to run the file without directly using the Kotlin CLI:

$ kotlin ./entry.main.kts 
vs
$ ./entry.main.kts

Running the script should print Oh Hi Mark as expected. Now we’re scripting! (If you get a permission error try $ chmod +x entry.main.kts).

Now we can add Maven dependencies with just a simple tag:

#!/usr/bin/env kotlin 

@file:DependsOn("io.ktor:ktor-server-core-jvm:2.3.9")
@file:DependsOn("io.ktor:ktor-server-netty-jvm:2.3.9")

build.gradle wishes it was this simple! Let’s flush out the rest of the file with some KTor boilerplate:

#!/usr/bin/env kotlin

@file:DependsOn("io.ktor:ktor-server-core-jvm:2.3.9")
@file:DependsOn("io.ktor:ktor-server-netty-jvm:2.3.9")

import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*


embeddedServer(Netty, port = 3000) {
  routing {
    get("/") {
      call.respondText("Kotlin Scripting!")
    }
  }
}.start(wait = true)

Run entry.main.kts and open 127.0.0.1:3000 and we see “Kotlin Scripting”. In only 19 lines we’ve made a Ktor server! No gradle, no IDEA files, just Kotlin.

Conclusion

Jetbrains’ Kotlin scripting is easy, intuitive, and convenient. Being able to add dependencies without a build file and run .kts from the command line without downloading anything extra is amazing. “1. Get from no project to a running server:” eazy-peazy. However, it’s not without it’s shortcomings. In the next part we’ll introduce the competing scripting implementation, KScript, and explore some of the general limitations of scripting in Kotlin. See you soon!