The place where random ideas get written down and lost in time.

2021-02-07 - Conductor2… Kotlin vs Groovy again

Category DEV

It’s been 2 years now that I started the rewrite of Conductor 2. Back then I chose Groovy, after trying Kotlin and deciding it wasn’t mature enough. In between, Kotlin usage has expanded a lot, so time to revisit.

(N.D.L.R.: the goal of the Conductor 2 project is to replace the custom language from Conductor 1 by a new DSL based on top of Groovy, or Kotlin. That would allow the script to have more flexibility.)

Overall Conductor2 with Groovy would work. Although the engine isn’t 100% operational, I rewrote most of the main automation script using Groovy and I updated it to a few new concepts that I miss in my current custom language.

The Conductor 2 design doc has more details on the new syntax.

Through the implementation, I evolved the syntax.

In conductor 1, we have basically a “global” namespace of “conditions --> actions”.

These can be expressed in the Groovy script using:

On { closure expression return a boolean } --> { actions block }

However I later rewrote the script to be more oriented for each route / block.

A route is defined as a sequence of blocks that must be traversed, and there are actions for each block. This avoids the soup of global state variables that must be maintained in Conductor 1 to achieve the same thing (e.g. “passenger train on this block going forward”). The new syntax looks like this:

Name = route {

        Route = [

                BlockNumber.forward {

                        onStart { actions }

                        onEnter { actions } then_after delay { actions }

        after delay { actions } then_after delay { actions }

} ] }

This scheme basically encodes most of the complexity in Conductor 1’s script, which is using timers and global state to limit actions to certain routes/blocks/directions.

There are only 3 limitations from using Groovy in the script:

  • Symbols declared using “def” are local to the script. That goes for variables and functions.
  • Keywords cannot be overloaded (which is OK I guess).
  • “-->” cannot be used as a method name. In Conductor 1, “-->” is a keyword in the language. I couldn’t have that in a DSL based on Groovy.

To circumvent the “-->” issue, when the script is loaded I am currently doing a string replacement from “-->” into “then”. The action “on { event } --> { action }” thus invokes the action.then(block) method. Both are interchangeable. That’s the real definition of syntactic sugar.

The “def variable” is a different issue. In the original Conductor 1 script, variables are defined as such:

        Name = Type Parameter

E.g.        B320 = sensor NS784

While using “def B320 = Block NS784” works in Groovy too, it makes the identified B320 local to the script and not readable by my engine. Consequently I can’t e.g. debug or display the variable. Although I can register all the block variables being created in a map, I would not have the name without repeating it as an argument. F.ex.“def B320 = Block B320 NS784”.

A variation would be to forgo creating variables in the script and let the engine register them in the context. E.g. the definition could simply be:

        Block B320 NS784

which would automatically create a variable named B320.

Finally the last issue with Groovy are the exception stack traces. They are incredibly kilometer-long, verbose, yet ironically totally meaningless -- I can’t figure out where an error is in the script based on the stack trace. So what’s the point, really?

One word on the groovy implementation. One thing I’m proud of is that all the engine is in “plain old boring Java” and not Groovy. All Groovy DSL examples use a Groovy source, but it turns out not to be a requirement. For each class exposed in the script, there are 2 Java classes:

  • The script’s top class a CScript.
    • The engine has a dagger autofactory “Script” matching that one CScript instance.
    • It provides for example a method block(name), which returns a “CBlock” type.
  • A script block is a “CBlock” class in the script, and a “Block” class in the engine.
    • The script CBlock only provides the API needed for the script and links to the engine underlying implementation Block instance to actually set and get state.

Thus the question is can all this be done using Kotlin.

Questions:

  • Do I want a “kotlin class” or a “kotlin script” or “kotlin as a DSL”.
  • Am I forced to implement the DSL in kotlin?
  • Can “-->” be a method name?

Note that if I’m rewriting Conductor from scratch (essentially what Conductor 2 does), it’s not out of the question to rewrite it fully in Kotlin as an exercise -- after all I’ve been wanting to do a Kotlin project for a while. It’s a bit orthogonal though, which is why the question is whether I am *forced* to implement the DSL in Kotlin. In the Groovy case, all the DSL examples are in Groovy yet it turned out to be trivial to use regular Java for the implementation.

Update: Bottom line is that Conductor 2 will remain using Groovy due to:

  • Kotlin DSL syntax does require parentheses and string quotation marks. Only Groovy has a special sugar syntax to avoid them (known as Groovy’s “command chain”)..
  • There is no way to find the variables declared in a kotlin DSL script (not without some ugly reflection at least), which is something I want to rely on here.
  • I believe I would be forced to implement the DSL in kotlin; which I did not see as a problem per se. The implementation was indeed syntactically more compact in Kotlin than Groovy, yet the structure was the same and would result in the same code basically.


 Generated on 2025-01-10 by Rig4j 0.1-Exp-f2c0035