DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Testing Java and Spring Applications With Kotlin: Low-Risk to Adoption
  • Automated CI/CD of Multiple Projects Using TeamCity’s Kotlin DSL
  • The Window Closes: Why I Switched to Kotlin Before the Codebase Got Too Big
  • Why Kotlin or Java Is the Wrong Question For Enterprises

Trending

  • Rebuilding Elasticsearch Under Load: Unsafe Watermarks and a Sampling Blind Spot
  • Batching Strategies: Throughput vs Latency in Practice
  • How to Format Articles for DZone
  • Fix the Target, Precompute Once: A Backend-Free Word-Ladder Solver With a BFS Distance Field

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  1. DZone
  2. Coding
  3. Languages
  4. Kotlin DSLs: The Basics

Kotlin DSLs: The Basics

Love Kotlin's inherent support for creating DSLs? Let's take a look at a few different approaches you can take when building a domain-specific language.

By 
Sarthak Makhija
·
Jun. 01, 18 · Tutorial
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
17.7K Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

A domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains. There is a wide variety of DSLs, ranging from widely used languages for common domains, such as HTML for web pages, down to languages used by only one or a few pieces of software.

Kotlin DSL

Kotlin provides first-class support for DSLs, which allows us to express domain-specific operations much more concisely than an equivalent piece of code in a general-purpose language.

Let’s try and build a simple DSL in Kotlin:

dependencies {
   compile("io.arrow-kt:arrow-data:0.7.1")
   compile("io.arrow-kt:arrow-instances-core:0.7.1")
   testCompile("io.kotlintest:kotlintest-runner-junit5:3.1.0")
}


This should be familiar to people using Gradle as their build tool. The above DSL specifies compile and testCompile dependencies for a Gradle project in a very concise and expressive form.

How Does Kotlin Support DSLs?

Before we get into Kotlin’s support for DSLs, let’s look at lambdas in Kotlin.

fun buildString(action: (StringBuilder) -> Unit): String {
   val sb = StringBuilder()
   action(sb)
   return sb.toString()
}


buildString() takes a lambda as a parameter (called action) and invokes it by passing an instance of StringBuilder. Any client code that invokes buildString() will look like the following code:

val str = buildString {
    it.append("Hello")
    it.append(" ")
    it.append("World")
}


A few things to note here:

  • buildString() takes the lambda as the last parameter. If a function takes a lambda as the last parameter, Kotlin allows you to invoke the function using braces { .. }, there is no need to use parentheses
  • it is the implicit parameter available in the lambda body, which is an instance of StringBuilder in this example

The information is good enough to write a Gradle dependencies DSL.

First Attempt at a DSL

In order to build a Gradle dependencies DSL we need a function called dependencies, which should take a lambda of type T as a parameter, where T provides the compile and testCompile functions.

Let’s try:

fun dependencies(action: (DependencyHandler) -> Unit): DependencyHandler {
    val dependencies = DependencyHandler()
    action(dependencies)
    return dependencies
}

class DependencyHandler {
    fun compile(coordinate: String){
        //add coordinate to some collection
    }
    fun testCompile(coordinate: String){
        //add coordinate to some collection
    }
}


dependencies is a simple function which takes a lambda accepting an instance of DependencyHandler as a parameter and returning Unit. DependencyHandler is the type T that has the compile and testCompile functions.

The client code for the above concept will look like:

dependencies {
    it.compile("") //it is an instance of DependencyHandler
    it.testCompile("")
}


Are we done? Not really.

The problem is the implicit parameter it used in the client code. Can we remove it?

In order to remove implicit parameters, we need to look at another concept known as a “Lambda With Receiver.”

Lambda With Receiver

A receiver in Kotlin is a simple type that is extended. Let’s see this with an example:

fun String.lastChar() : Char = 
                  this.toCharArray().get(this.length - 1)


We have extended String to have lastChar() as a function, which means we can always invoke it as:

"Kotlin".lastChar()


Here, String is the receiver type and this used in the body of lastChar() is the receiver object. These two concepts can be combined to form a Lambda With Receiver.

Let’s rewrite our buildString function using a lambda with receiver:

fun buildString(action: StringBuilder.() -> Unit): String {
    val sb = StringBuilder()
    sb.action()
    return sb.toString()
}


  • buildString() takes a lambda with receiver as a parameter.
  • StringBuilder is the receiver type in the lambda (action parameter).
  • The way we invoke the action function is different this time. Because action is an extension function of StringBuilder we invoke it using sb.action(), where sb is an instance of StringBuilder.

Let’s create a client of the buildString function:

val str = buildString {
    this.append("Hello") //this here is an instance of StringBuilder
    append(" ")
    append("World")
}


Isn’t this brilliant? Client code will always have access to this while invoking a function that takes a lambda with receiver as a parameter.

Shall we rewrite our Gradle dependencies DSL code?

Another Attempt at a DSL

fun dependencies(action: DependencyHandler.() -> Unit): DependencyHandler {
    val dependencies = DependencyHandler()
    dependencies.action()
    return dependencies
}

class DependencyHandler {
    fun compile(coordinate: String){
        //add coordinate to some collection
    }
    fun testCompile(coordinate: String){
        //add coordinate to some collection
    }
}


The only change we have made here is in the dependencies function, which takes a lambda with receiver as the parameter. DependencyHandler is the receiver type in the action parameter, which means the client code invoking the dependencies function will always have access to the instance of DependencyHandler.

Let’s see the client code:

dependencies {
   compile("")     //same as this.compile("")
   testCompile("")
}


We are able to create a DSL using a lambda with receiver as a parameter to a function.

Operator Function invoke()

Kotlin provides an interesting function called invoke, which is an operator function. Specifying an invoke operator on a class allows it to be called on any instances of the class without a method name.

Let’s see this in action:

class Greeter(val greeting: String) {
    operator fun invoke(name: String) {
        println("$greeting $name")
    }
}

fun main(args: Array<String>) {
    val greeter = Greeter(greeting = "Welcome")
    greeter(name = "Kotlin")
    //this calls the invoke function which takes String as a parameter
}


A few things to note about invoke() here. It:

  • Is an operator function.
  • Can take parameters.
  • Can be overloaded.
  • Is being called on the instance of a Greeter class without method name.

Let’s use invoke in building a DSL.

Building DSLs Using Invoke Functions

class DependencyHandler {
    fun compile(coordinate: String){
        //add coordinate to some collection
    }
    fun testCompile(coordinate: String){
        //add coordinate to some collection
    }
    operator fun invoke(action: DependencyHandler.() -> Unit): DependencyHandler {
        this.action()
        return this
    }
}


We have defined an operator function in DependencyHandler, which takes a lambda with receiver as a parameter. This means invoke will automatically be called on instances of DependencyHandler and client code will have access to the instance of DependencyHandler.

Let’s write the client code:

val dependencies = DependencyHandler()
dependencies { //as good as dependencies.invoke(..)
    compile("")
    testCompile("")
}


invoke() can come in handy while building DSLs.

Conclusion

  • Kotlin provides first-class, typesafe support for DSLs.
  • One can create a DSL in Kotlin using:
    • Lambdas as function parameters.
    • A lambda with receiver as a function parameter.
    • An operator function invoked along with a lambda with receiver as a function parameter.

References

  • Kotlin In Action
Kotlin (programming language) Domain-Specific Language Receiver (information theory) Dependency

Published at DZone with permission of Sarthak Makhija. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Testing Java and Spring Applications With Kotlin: Low-Risk to Adoption
  • Automated CI/CD of Multiple Projects Using TeamCity’s Kotlin DSL
  • The Window Closes: Why I Switched to Kotlin Before the Codebase Got Too Big
  • Why Kotlin or Java Is the Wrong Question For Enterprises

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook