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
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Automated CI/CD of Multiple Projects Using TeamCity’s Kotlin DSL
  • While Performing Dependency Selection, I Avoid the Loss Of Sleep From Node.js Libraries' Dangers
  • Why I Started Using Dependency Injection in Python
  • Apache Doris vs Elasticsearch: An In-Depth Comparative Analysis

Trending

  • A Deep Dive Into Firmware Over the Air for IoT Devices
  • Enforcing Architecture With ArchUnit in Java
  • How to Ensure Cross-Time Zone Data Integrity and Consistency in Global Data Pipelines
  • Accelerating Debugging in Integration Testing: An Efficient Search-Based Workflow for Impact Localization
  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 user avatar
Sarthak Makhija
·
Jun. 01, 18 · Tutorial
Likes (12)
Comment
Save
Tweet
Share
17.2K Views

Join the DZone community and get the full member experience.

Join For Free

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, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Automated CI/CD of Multiple Projects Using TeamCity’s Kotlin DSL
  • While Performing Dependency Selection, I Avoid the Loss Of Sleep From Node.js Libraries' Dangers
  • Why I Started Using Dependency Injection in Python
  • Apache Doris vs Elasticsearch: An In-Depth Comparative Analysis

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

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 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!