🚀 · What makes a DSL

6 min read · Updated on by

Read on to discover a simple definition of a DSL, an example in both Java and Kotlin, and a demonstration of the unexpected way a DSL actually becomes a DSL.

To some, DSLs — domain specific languages — can appear to have a mystical quality. One of those things that you hear about, but maybe treat as a form of esoteric programming knowledge which has no business being used in the real world. This is actually far from true, and in fact we will see that DSLs are just a special name for something you already do every day. However, in order to banish any sort of black magic feelings you may have associated with DSLs, let’s start by giving a simple definition that strips away the magic:

A DSL is a set of functions which are named after intuitively clear behaviors in a given context.

That’s it. For instance, in the context of list creation, we might have the functions addaddAlladdIfaddUnless, which together would constitute a DSL. It is intuitively clear what they do.

What’s important to understand is that Kotlin has no special features that somehow allow you to design DSLs that are not possible in other languages. The only thing that’s special about Kotlin is that it allows DSLs to be used really nicely. And you might be surprised to learn that you already know everything you need — there are no new Kotlin features involved.

To really drive home the idea that DSLs are nothing special, and “can be done” in almost any language, we’ll first implement the above-mentioned list-creation DSL in Java.

Simple Java example

There are two steps to creating a DSL — design and implementation. Designing a DSL basically means deciding which words to use, and that requires some thought about the problem domain. However, we will see later that it is in fact very similar to the thought process that goes into (or should go into) designing a class. Indeed, we will see that in fact, every properly-designed class basically defines its own DSL.

Implementing the DSL is the “easy” part — you just implement the words you decided on, and bundle them together somehow. And since the only way you can bundle functions together in Java is by putting them in a class, that’s what we’ll do. I’m going to syntax-highlight this example, to make it easier to read.


//sampleStart
import java.util.*;
import java.util.function.*;

class ListBuildingDSL<T> {
  private final List<T> list = new ArrayList<>();

  public static <T> ListBuildingDSL<T> start() {
    return new ListBuildingDSL<>();
  }

  public ListBuildingDSL<T> add(T element) {
    list.add(element);
    return this;
  }

  public ListBuildingDSL<T> addIf(T element, BiPredicate<T, List<T>> predicate) {
    if (predicate.test(element, list)) {
      list.add(element);
    }
    return this;
  }

  public ListBuildingDSL<T> addUnless(T element, BiPredicate<T, List<T>> predicate) {
    if (!predicate.test(element, list)) {
      list.add(element);
    }
    return this;
  }

  public ListBuildingDSL<T> add(Collection<T> elements) {
    list.addAll(elements);
    return this;
  }

  public List<T> getList() {
    return list;
  }
}

class Example {
  public static void main(String[] args) {
    var myList = ListBuildingDSL.<Integer>start()
        .add(3)
        .add(5)
        .addIf(7, (e, list) -> list.size() < 2)
        .getList();
  }
}
//sampleEnd

Ta-da.

At this point, you’re probably overwhelmingly underwhelmed. Basically, what we did is create a builder. Woo.

Kotlifying the example

Let’s migrate this to Kotlin, and see if we can make things prettier and more “DSL-like”.


//sampleStart
class ListBuildingDSL<T> {
    private val _list: MutableList<T> = mutableListOf()
    val list: List<T> get() = _list
    
    fun add(element: T) = apply {
      _list.add(element)
    }

    fun addIf(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addUnless(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (!predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addAll(elements: Collection<T>) = apply {
        _list.addAll(elements)
    }
}

val myList = ListBuildingDSL<Int>()
    .add(3)
    .add(5)
    .addIf(7) { _, list -> list.size > 2 }
    .list
//sampleEnd
fun main() {
    val poem = """
        When you're in the labyrinth of code's maze,
        Kotlin's syntax is the guiding blaze.
        With paths and twists, a journey so vast,
        In the coding labyrinth, it's steadfast!
    """.trimIndent()
    println(poem)
}

(Just a quick aside, even though this is only a direct port of the Java version, notice how much cleaner the code is already? Kotlin is awesome!)

It still seems like we’re nowhere near what we would consider a DSL.

Now, let’s make one small change:


class ListBuildingDSL<T> {
    private val _list: MutableList<T> = mutableListOf()
    val list: List<T> get() = _list
    
    fun add(element: T) = apply {
      _list.add(element)
    }

    fun addIf(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addUnless(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (!predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addAll(elements: Collection<T>) = apply {
        _list.addAll(elements)
    }
}
//sampleStart
val myList = with(ListBuildingDSL<Int>()) {
    add(3)
    add(5)
    addIf(7) { _, list -> list.size > 2 }
    list
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the chef in the code's kitchen,
        With extension properties, it spices the vision.
        From recipes to flavors, a feast so fine,
        In the world of development, it's the dine!
    """.trimIndent()
    println(poem)
}

Woah, that escalated quickly. With one small change, we went from “nothing-remotely-like-a-DSL” to “sort-of-like-a-DSL”.

We’re still not quite there. One thing that really looks just horrible is having to return list at the end. So let's fix that:


class ListBuildingDSL<T> {
    private val _list: MutableList<T> = mutableListOf()
    val list: List<T> get() = _list
    
    fun add(element: T) = apply {
      _list.add(element)
    }

    fun addIf(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addUnless(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (!predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addAll(elements: Collection<T>) = apply {
        _list.addAll(elements)
    }
}
//sampleStart
inline fun <T> buildList(
    listBuildingDSLInstance: ListBuildingDSL<T>, 
    block: ListBuildingDSL<T>.() -> Unit
) = 
    listBuildingDSLInstance
    	.apply(block)
    	.list

val myList = buildList(ListBuildingDSL<Int>()) {
	add(3)
	add(5)
	addIf(7) { _, list -> list.size > 2 }
}
//sampleEnd
fun main() {
    val poem = """
        In the coding harmony, Kotlin's the chord,
        With extension functions, it's never ignored.
        From notes to melodies, a musical spree,
        In the world of programming, it's the key!
    """.trimIndent()
    println(poem)
}

And while we’re at it, do we really need to pass a new instance of ListBuildingDSL every time?


class ListBuildingDSL<T> {
    private val _list: MutableList<T> = mutableListOf()
    val list: List<T> get() = _list
    
    fun add(element: T) = apply {
      _list.add(element)
    }

    fun addIf(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addUnless(element: T, predicate: (T, List<T>) -> Boolean) = apply {
        if (!predicate(element, _list)) {
            _list.add(element)
        }
    }

    fun addAll(elements: Collection<T>) = apply {
        _list.addAll(elements)
    }
}
//sampleStart
inline fun <T> buildList(
    block: ListBuildingDSL<T>.() -> Unit
) = 
    ListBuildingDSL<T>()
    	.apply(block)
    	.list

val myList = buildList<Int> {
	add(3)
	add(5)
	addIf(7) { _, list -> list.size > 2 }
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the painter in the code's canvas,
        With colors and strokes, a visual compass.
        From palettes to hues, a masterpiece true,
        In the coding exhibition, it's the view!
    """.trimIndent()
    println(poem)
}

And, frankly, we could pretty much say we’re done — this is a DSL by any reasonable definition. It allows us to build an immutable list using “mutable semantics”, which is cool (in fact, it’s cool enough that it’s part of the standard library).

Crucially however, notice that we didn’t touch the class definition at all. Everything inside the class stayed exactly the same and in fact, almost nothing would change if we just used the previous Java version and called it from Kotlin (apart from nullability).

So what exactly happened? Continue to the next article to find out!

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer