Scope Control With @DslMarker Annotation. Kotlin DSLs.

This time, I will show you how to control scope with @DslMarker annotation when creating your DSLs in Kotlin.
Image is a featured image for an article about how to control scope in Kotlin DSL using @DslMarker annotation and consist of Kotlin logo in the foreground and a blurred photo of a city in the background.

Hello and welcome to the next lesson! This time, I will show you how to control scope with @DslMarker annotation when creating your DSLs in Kotlin.

Note: this article is based on my Complete Kotlin Course lesson, which I highly encourage you to check out if you are looking for a comprehensive Kotlin guide.

If this is your first meeting with DSLs, then I would recommend you to check out my other article, in which I explain and show how to implement Kotlin DSL step-by-step.

Video Tutorial

If you prefer video content, then check out my video:

If you find this content useful, please leave a subscription  😉

Uncontrolled Scope

But before we dive into the solution with Kotlin @DslMarker, let’s understand what problem it solves first.

So as the first step, let’s prepare a simple DSL with Board, Task, Author, and Comment classes:

enum class BoardColor {
  BLACK, WHITE, GREEN, BLUE
}

class Board {
  var title: String = ""
  var color: BoardColor = BoardColor.BLUE
  val tasks: MutableList<Task> = mutableListOf()

  fun task(init: Task.() -> Unit) {
    val task = Task().apply(init)
    tasks.add(task)
  }
}

class Task {
  var title: String = ""
  var description: String = ""
  val comments: MutableList<Comment> = mutableListOf()

  fun comment(init: Comment.() -> Unit) {
    val comment = Comment().apply(init)
    comments.add(comment)
  }
}

class Comment {
  var comment: String = ""
  var author: Author = Author()

  fun author(init: Author.() -> Unit) {
    val author = Author().apply(init)
    this.author = author
  }
}

class Author {
  var name: String = ""
}

fun board(init: Board.() -> Unit): Board =
  Board()
    .apply(init)

With the following Kotlin DSL implementation, we would expect that we could introduce different Boards, with Tasks inside them, which would have some Comments written by Authors.

But let’s take a look at the following code:

fun main() {
  val board = board {
    task {
      task {
        task {
          comment {
            task { }
            author {
              task { }
              comment {
                task { }
              }
            }
          }
        }
      }
    }
  }
}

Will it compile? Unfortunately, yes.

We invoke a task within a task, within a task, and so on. Moreover, we can invoke the task, or comment functions inside the author

And that’s because, by default, we can call the methods of every available receiver, which can lead to such situations. 

Kotlin @DslMarker To The Rescue

At this point, we already know what the issue is and what we will use to fix it.

But what exactly does the @DslMarker do in Kotlin?

Well, in practice, we use this annotation to introduce our new custom annotations. Then, we make use of them to mark classes and receivers, thus preventing receivers marked with the same annotation from being accessed inside one another.

This way, we won’t be able to access members of the outer receiver (like the task function, which is declared inside the Board class from the Task class instances).

Let’s consider the updated example:

@DslMarker
annotation class BoardDsl

enum class BoardColor {
  BLACK, WHITE, GREEN, BLUE
}

@BoardDsl
class Board {
  var title: String = ""
  var color: BoardColor = BoardColor.BLUE
  val tasks: MutableList<Task> = mutableListOf()

  fun task(init: Task.() -> Unit) {
    val task = Task().apply(init)
    tasks.add(task)
  }
}

@BoardDsl
class Task {
  var title: String = ""
  var description: String = ""
  val comments: MutableList<Comment> = mutableListOf()

  fun comment(init: Comment.() -> Unit) {
    val comment = Comment().apply(init)
    comments.add(comment)
  }
}

@BoardDsl
class Comment {
  var comment: String = ""
  var author: Author = Author()

  fun author(init: Author.() -> Unit) {
    val author = Author().apply(init)
    this.author = author
  }
}

@BoardDsl
class Author {
  var name: String = ""
}

fun board(init: Board.() -> Unit): Board =
  Board()
    .apply(init)

fun main() {
  val board = board {
    task {  // OK
      task {  // Does not compile
        task {  // Does not compile
          comment {  // OK
            task { }  // Does not compile
            author {  // OK
              task { }  // Does not compile
              comment {  // Does not compile
                task { }  // Does not compile
              }
            }
          }
        }
      }
    }
  }
}

Firstly, we introduce the @BoardDsl annotation, which uses the @DslMarker.

Following, we must annotate every class in our hierarchy with our new annotation- this way, we make the Kotlin compiler “aware” of the hierarchy in our DSL.

Lastly, we can clearly see that the code will not compile whenever we try to nest the unwanted type inside another.

Scope Control With Kotlin @DslMarker Summary

Basically, that’s all for this tutorial on how to control scope when implementing a Kotlin DSL using the @DslMarker summary.

If you enjoy such a short, practice-focused learning approach then do not forget to check out my course and let me know in the comments section.

If you’d like to learn a bit more about the annotation itself, then the documentation may be useful to you too.

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy