Use Conventions to Improve Fluency

When designing DSLs we should aim for the syntax to read like a natural language or a data input file. DSLs should be fluent and not feel like old C or C++ code. The code should be easy for nonprogrammers to write and just about anyone to read.

Suppose our banking application’s DSL needs to process a series of transactions that are posted as a stream of data. An example stream of data may look like this:

 account number 12345678 deposit 10025
 account number 12345678 deposit 15045

There are no unnecessary elements in that data file—it’s plain, simple, and to the point. As it turns out, that’s 100% bonafide Kotlin syntax, except we have to apply some conventions and bend some rules just a little to get it working.

Let’s focus on designing the code to make that sample data file work. First we need a way to process the contents. No worries, Kotlin can take care of that with no effort needed on our part. Save the data into a file named transactions.kts.

To design the DSL to process that data file, we have to wear a pair of special glasses and map the syntactical structure into constructs in Kotlin. Let’s take the first line:

 account number 12345678 deposit 10025

In programming speak that’s really:

account.number(12345678).deposit(10025);

Let’s put that into words: on an account object, invoke a number function and pass the account number 12345678 to it. Then on the result of that call, invoke the deposit function and pass the amount 10025.

Kotlin doesn’t care about ;, so you don’t have to worry about that. In Kotlin we can leave out the dot and the parentheses if a function in a class or a singleton is defined as infix and takes exactly one parameter.

That almost answers the question about how we’d design the code to process the data file. Well, almost…

number is a function and so is deposit. 12345678 and 10025 are numbers. But where does account come from? We don’t want the user providing the data to worry about creating instances. Thus, we can bend the naming conventions of Kotlin just a bit and give a class an all-lowercase name, account, instead of naming it Account. Alternatively, we can create the class as Account, following the expected conventions, and define a type alias to let Kotlin know that when it sees account it should read it as Account. In previous examples we broke away from the conventions, but in this example, we’ll take this other approach to illustrate the use of type alias. To avoid creating an explicit instance, we can make number() a function in the companion object of Account.

We’ll place the code necessary to process the DSL in a file named Account.kt:

 class​ Account(​val​ accountNumber: Int) {
 companion​ ​object​ {
 infix​ ​fun​ ​number​(accountNumber: Int) = Account(accountNumber)
  }
 
 infix​ ​fun​ ​deposit​(amount: Int) =
  println(​"depositing $${amount/100.0} into account $accountNumber"​)
 
 infix​ ​fun​ ​withdraw​(amount: Int) =
  println(​"withdrawing $${amount/100.0} from account $accountNumber"​)
 }
 
 typealias​ account = Account

The Account class has a read-only property named accountNumber. The companion object holds the number function and it returns an instance of account. The deposit and withdraw functions can do the actual processing, but in this example these functions merely report their call using a println.

Now let’s see how our efforts can be used to process the data file:

 account number 12345678 deposit 10025
 account number 12345678 deposit 15045

We first have to compile the account class and then execute the transactions.kts file as a script, like so:

 kotlinc-jvm -d classes Account.kt
 
 kotlinc-jvm -classpath classes -script transactions.kts

Once we compile the class, we can refer to it using the classpath setting when running the Kotlinc-jvm tool in the script mode. The output from the execution is:

 depositing $100.25 into account 12345678
 depositing $150.45 into account 12345678

When executing a script, we can add to the classpath the locations of compiled classes or JAR files. As a result, our script can refer to any code that is available on the classpath, whether that comes from the JDK, from the Kotlin standard library, or any third party code.

Before we move on, let’s recap what we’ve done. The ability to run a DSL script where the necessary supporting classes and functions are bundled into JAR files greatly removes the burden to process DSL files.

In our example, the script refers to an Account companion object that is located in the classpath during runtime. With no effort we were able to use the functions of the Account class and the companion object from within the DSL script.

By introducing a companion object within the class and using type alias, we easily managed to accommodate the syntax account number 12345678 without needing to create any objects explicitly. This approach made it easier for the user of the DSL to both write and read the syntax. Once the number() function is called, it returns an instance of the Account class, and then the method chaining kicks in for the call to the deposit() function.

The net result of the design is an easy-to-process DSL with data-like fluent syntax.

We saw how fluency removes the noise and clutter and makes the syntax lightweight. While fluency is cosmetic, the ability to invoke functions or access properties with domain-specific names instead of generic names can add to the strength of DSLs. We’ll see how to get domain specific in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.12.240