Hey guys! I'm a big fan of Kotlin DSL and its type...
# kotlin-dsl
r
Hey guys! I'm a big fan of Kotlin DSL and its typesafe accessors in particular 😉 Although, one of the documentation examples got me thinking recently:
Copy code
plugins {
    application                                     // by name
    java                                            // by name
    id("java")                                      // by id - recommended
    id("org.jetbrains.kotlin.jvm") version "1.9.0"  // by id - recommended
}
Why is the non-typesafe plugin application
id("java")
recommended over the typesafe accessor
java
? For me, the typesafe one is both easier to read and easier to write (thanks to IDE autocompletion) + there is no way to make a typo. I tend to use typesafe accessors wherever possible in my buildscripts and I don't see any reason to prefer the non-typesafe plugin application 🤔
p
I don’t know if this is the reason but the id syntax is faster because it uses a special handwritten parser under the hood while the typed (and alias one) uses the Kotlin compiler. But I don’t know how fast the parser is and if you really notice a difference in a big project
☝️ 2
thank you 1
l
That sounds like an outstanding improvement to the Gradle team, right? 🙂 The desired state would be: 1. A single, idiomatic and documented way to apply plugins. 2. Documented typesafe accessors which yields optimum performance. (The task of DSL:s is to bridge high-performant API calls with a readable and typesafe syntax, so running optimal calls using a typesafe accessor is really the main task of a DSL) 3. A documented and idiomatic way to create your own DSL syntax to apply custom plugins in the same way (performance and readability) as Gradle's core plugins. I believe you should be able - after the above - to get optimum performance for either of the two snippets below:
Copy code
plugins {
    companyStandardJvmProject
}
or
Copy code
plugins {
    java
}
You agree with the 3 points above, @Vampire?
v
Not really. You get type-safe accessors for things that are already on the classpath. So built-in plugins are on the "core" classpath so you get accessors. If you add a plugin for example to the settings script classpath, you also get type-safe accessors for those for the build scripts for any plugin including any 3rd-party plugin. The task of a DSL is not to be high-performant. The task of a DSL is to be user-friendly. To evaluate the Kotlin DSL you have to send it through the Kotlin compiler and execute it. If you can instead just use a simple text-parser, this is faster. So if you care about any bit of performance, you have to make a trade-off between user-friendliness (using accessors and version catalogs) and performance (using only things the custom parser can handle).
But just my 2ct, I'm not authorative
l
The task of a DSL is not to be high-performant.
The task of a DSL is to be user-friendly.
That sounds like a rather oddly crafted DSL. Of course it should be user-friendly - otherwise a DSL would not be needed at all. The Gradle engineers control the DSL, the compilation result for it and the execution environment wherein it runs. Why should a DSL's compilation result be anything less than optimal performance?
v
The compilation result surely is, but the point is, that you need to compile it, and you need to run it, and both even individually needs much more time than being able to do a simple string-parse.
l
I cant see the difference. 1.
id("foobar")
must be compiled and executed. 2. The extension function
foobar
must also be compiled and executed. An example of a simple DSL construct which would yield
Copy code
plugins {
  foobar
}
… to do exactly the same thing as the
id("foobar")
call, compilation-wise and in other ways, would be:
Copy code
val PluginDependenciesSpec.foobar: PluginDependencySpec
  get() = id("foobar")
.. but the latter would be typesafe, safe from typos, able to be documented and IDE-friendly
v
Your 1. is wrong, that's the point
l
It is a kotlin call, so before it can be run it must be compiled, yes?
v
That's the point, it does not need to run
Not when the quick-parser is used
Which can only be used when you do not use certain things
Then it can just textually parse the
plugins { ... }
block
Which is faster than compiling and running it
l
A quick parser is not a Kotlin compilation step, then. Text-macro handler?
v
Again, ignore that it is Kotlin.
For a certain task, Gradle needs to interpret the
plugins
block on its own
As long as some things are not used like version catalogs or type-safe accessors, it can just use a text-parser
If anything is used that the parser cannot handle, it needs to compile and run it
But just using the text-parser is simply faster, fullstop
l
So … that text parser needs to invoke Gradle interfaces implemented in JAR:s, right? How can that be done unless the call is made in bytecode?
Actually running the plugin’s apply method is done in a JVM.
what am I missing here? Even a parser which parses a script file and then invokes a method or two in JAR:s require some JVM support to do so.
p
Of course Gradle is executed in a JVM and parsing the plugins block using the handwritten parser is done in the same JVM. But parsing a few lines String in memory is incredible fast, compared to starting the Kotlin compiler, that needs a another (daemon) JVM, physical files (you need to write down the few lines), a classpath, setup all the compiler options and parse the output files.
l
Sure - if the only thing in the buildfile you need to do is to add one plugin, that comparison holds true. Does it hold true even if you need to actually do other things in the buildfile? Not only applying more plugins, but actually configure tasks etc? (assuming, in that case, that the Kotlin compiler is required in those cases nonetheless)
p
The plugins block will be extracted from the actual build file, because to generate type safe (task) accessors, you need to execute the block first. So you need to compile the file twice with the (slow) Kotlin compiler, or only once and use a in memory parser.
v
So … that text parser needs to invoke Gradle interfaces implemented in JAR:s, right?
No, it just needs to parse that block and that's it.
Again, to finally execute the build script, it of course needs to be compiled and executed. But before that Gradle already needs to evaluate the
plugins { ... }
block to find out which type-safe accessors to generate for the remaining build script. For this It transplants the
plugins { ... }
block to a dummy project, executes it and then investigates which things the plugins applied in that block added to generate type-safe accessors for exactly those things that are really available. For that step, you can save the Kotlin compilation and execution time by simply using the string-parser to get the plugins to apply and apply them within the JVM or whatever.
As long as the
plugins { ... }
block does not use something that parser cannot handle.