1

I'm looking into Kotlin DSL for a project, and I'm wondering if there is a way to weave constraints into the DSL. I'd like these to be compilation constraints if possible, I'm aware that it's possible to throw exceptions for these cases but my ideal is compiler enforcement.

Using the HTML DSL example in the docs (https://kotlinlang.org/docs/type-safe-builders.html):

  • can function ordering be enforced?
fun result() =
    html {
        body { // cause a compiler error, body used before head
          ...
        }
        head { ... }
  • can duplicates be disallowed?

fun result() =
    html {
        head { ... }

        head { // cause a compiler error, disallow duplicate head
          ...
        }
        body { ... }
  • can existence be required?
html {
    head { ... } 
    // Compiler error, body is required 
}

Again, ideally these are compiler errors, not exceptions. Is this possible?

I tried with DslMarker but didn't find a way to get it working.

Thanks!

2
  • 2
    I don't believe these constraints are possible at compile-time, but you can have runtime code which toggles a flag/determines the order in which they're called (for e.g. using enums, adding these enums when you call a certain DSL to a set/list and then checking that the order is what you expect when you build the resulting DSL) Commented Jul 28, 2024 at 9:13
  • This question is too broad. I think you should focus on one issue at a time. For example, my answer here shows how you can require the existence of some call. Commented Jul 28, 2024 at 11:57

1 Answer 1

1

I don't think this is possible. We can either create some kind of a compiler plugin, or we can use a more classic approach where we chain builder functions, but instead of using only a single builder type, we use multiple of them. It doesn't fit very well with Kotlin DSL though and it looks kind of ugly:

fun main() {
    // compiles
    html {
        head {}
        .body {}
    }

    html {} // doesn't compile

    html {
        body {} // doesn't compile
    }

    html {
        head {} // doesn't compile
    }

    html {
        head {}
        .head {} // doesn't compile
        .body {}
    }

    html {
        head {}
        .body {}
        .body {} // doesn't compile
    }
}


fun html(block: HtmlScope.() -> HtmlWithBodyScope) {}

interface HtmlScope {
    fun head(block: () -> Unit): HtmlWithHeadScope
}

interface HtmlWithHeadScope {
    fun body(block: () -> Unit): HtmlWithBodyScope
}

interface HtmlWithBodyScope

Also, it is still possible to do:

html {
    head {}
    head {}
}

It may be better to not use lambdas at all and go fully classic:

htmlBuilder()
    .head()
    .body()

This way it is much harder to call head() twice as we would have to assign the builder to a variable explicitly.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.