en ru de
Victor Kropp

Expressive code with DSLs in Kotlin. Part 2

In the first part of this tutorial, I’ve shown how to create custom internal DSL, which helps to convert an unstructured imperative code to expressive structured and declarative. In this blog post, I’ll improve it further.

Dates manipulation

I know well that the Christmas is on December 25, but the question I always ask myself at the beginning of December when is the first Advent Sunday. There are four of them, and the last one is on Sunday before Christmas. So the first one can be anytime between November 27 and December 3. Let’s find it out:

val `4th Advent` = Sunday before Christmas

Variable names can neither start with a digit nor contain a space, in Kotlin you can escape them with backticks. Do not overuse this feature, though.

Looks nice, isn’t it? It doesn’t work out of the box, though. To help compiler figure it out, let’s declare the variable Sunday and infix function before (we’ve seen this trick before):

val Sunday = DayOfWeek.SUNDAY
infix fun DayOfWeek.before(date: LocalDate): LocalDate = date.with(TemporalAdjusters.previous(this))

To create events in our calendar for all Advent Sundays we need to find the first one (3 weeks earlier) and iterate over them. It can be done in the following way:

(`4th Advent` - 3 * week .. `4th Advent` every week).forEachIndexed { i, Advent ->
  event {
    title = "$i Advent"
    date = Advent
  }
}

The most interesting is the first line, which is packed with neat tricks, following lines just repeat the creation process of an event for Christmas. First, with the help of overloaded multiplication operator, we create an object, representing three weeks:

val week: Period = Period.ofDays(7)
operator fun Int.times(p: Period): Period = p.multipliedBy(this)

operator keyword allows us to call extension function times with an asterisk, like any ordinary multiplication. LocalDate class is defined in Java and already has minus function, Kotlin allows to call it with a - sign without any additional modifications, so that 4th Advent - 3 * week compiles to 4th Advent.minus(3 * week)

Ranges

Two dots denote a range in Kotlin. And since its a regular class we can define an extension function every on it:

infix fun ClosedRange<LocalDate>.every(period: Period) = buildSequence {
  var current = start
  do {
    yield(current)
    current += period
  } while (current <= endInclusive)
}

Without going into much detail here, it creates a sequence of dates with a given period in-between, which we can then iterate with forEachIndexed, or standard for loop, of course.

invoke convention

This code will look much better if we can avoid this .forEachIndexed call altogether:

(`4th Advent` - 3 * week .. `4th Advent` every week) { i, Advent ->
  event {
    title = "$i Advent"
    date = Advent
  }
}

And we can! invoke operator allows us to call an object or an expression as a method. In our case it will just delegate to forEachIndexed:

operator fun <T> Sequence<T>.invoke(body: (Int, T) -> Unit) = forEachIndexed(body)

Performance

DSLs in Kotlin are statically compiled code; they do not require any dynamic resolution whatsoever. They do not use reflection either. Everything we’ve seen here is just function calls. However, passing lambda functions here and there can be a performance problem because each of them requires creating a closure. To solve this problem, we can mark DSL functions with inline keyword. They will be inlined at the call site together with a lambda function they take as a parameter.

Scopes

As I said before the whole DSL is based on simple functions, which can be called from everywhere, for example, such code is allowed by default:

calendar {
  event {
    event {}
  }
}

Nesting events don’t make much sense. Since Kotlin 1.1 we can control scope of the implicit receivers.

@DslMarker
@Target(AnnotationTarget.TYPE)
annotation class CalendarDsl

fun calendar(builder: (@CalendarDsl ICalendar).() -> Unit) = ICalendar().apply(builder)
fun ICalendar.event(builder: (@CalendarDsl VEvent).() -> Unit) = addEvent(VEvent().apply(builder))

To disallow such calls, we define an annotation, mark it with @DslMarker annotation, and then apply it types which define logical scopes in our DSL. After that nested event may be called only with an implicit receiver.

And that’s it! Our DSL is finished. You can find the full source code of this example GitHub. The recording of my talk about this DSL is available on YouTube.

Thanks for reading! I wish you Merry Christmas and Happy New Year!

kotlindslprogramming
Send

Subscribe to all blog posts via RSS, follow me @kropp and join my Telegram channel.