Hacking Kotlin — Writing a DSL

Andrew Snyder
9 min readMar 11, 2021
source: kotlinlang.org

Intro

In this post I wanted to share something I’m really excited about that Kotlin not only allows for but makes a breeze! That “thing” is hacking together your own Domain Specific Language or DSL for short.

On its own, this article will not give you a step-by-step guide to apply to your own projects but rather highlight some features of Kotlin that I took advantage of to build a DSL in my own project. Doing this proved to be invaluable for me and my intention is to provide an “Aha!” moment, perhaps opening a new door or way of thinking when setting up your own project architecture and utilities.

The DSL I will be documenting in this article was pulled from a project I built recently using Kotlin/JS to make a PokeDex web app. Some of the sections may be unfamiliar because of the variation but it is still Kotlin at its core. If you are familiar with Kotlin, you’ll be able to follow along. I will go through a few key points briefly before we jump into the main content.

The big differences with Kotlin/JS that are relevant to this article are the usage of dynamic, attrs, and props.

  • The dynamic type is essentially a non-compiler checked version of Any. You set values on a dynamic object without type-safety and must handle it cautiously when accessing those values.
  • attrs is the field on a UI element where you apply attributes like text color, width, etc.
  • props are a feature of React that can be thought of as whatever data you pass from parent to child component like arguments in a function call.

Objective

I’d like to start by explaining what problem this DSL solved for me and then go into some further detail of the code before reeling it back in to give a nice closing summary.

I was working in React and trying to find a practical, efficient way to pass props from one component to another. There are some rudimentary ways to do this but they are very loose and I don’t like loose. I like jumping around in my codebase and being able to piece together exact behavior without needing to build or run the code. With that goal in mind, my DSL requirements were:

  • A way to build a component with a lightweight, idiomatic syntax.
  • Have a set of required props that the component needs.
  • Supply any number of optional props that could be applied or “drilled” down the chain.
  • The option to pass components to be rendered as children.
  • Be as generic as possible and easily be able to make component classes compatible.

I wanted to be able to call pokemonDetailView to build my component and then supply what is needed within its scope. This conforms to the syntax and idioms of the rest of the Kotlin/React wrapper which you may recognize is very similar to JetBrain’s Compose UI.

I had an idea of how I wanted this to be designed, and then tweaked the implementation until I got it just right. Here’s what the usage ended up looking like; exactly what I was trying to achieve.

override fun RBuilder.render() {
browserRouter {
switch {

route<IdProps>("/pokemon/:id") { props ->

//call the component builder
pokemonDetailView { optionalProps, children ->

//supply optional props
optionalProps {
optionalValue1 = "Testing, 1..2..3.."
optionalValue2 = 7357
}

//provide children to the target component
children {
div {
+"Hello, DSL!"
}
}

//end with returning the required props class
PokemonDetailProps(props.match.params.id)
}
}
}
}
}

Key Concepts

Understanding some key concepts:

The following sections will get into the details of the DSL, the main concepts to understand going into it are

  • Scoped functions, like String.() -> Unit
  • Generic types, ceilings, Type parameters, etc
  • Passing functions as parameters (::function)
  • Type Alias

If you are familiar with all of these, feel free to skip the following overview and get to the Component Builder DSL.

Otherwise, let’s take a brief look at each concept.

Scoped Functions

Kotlin treats function signatures as types and as such there are a few different variations.

For example, take a look at these two function types and how they differ in usage.

fun main() {

// Using a typical function argument
// with a single parameter, the default
// scope capture will be 'it'.
"Hello It".scopeIt { //it: String
println(it)
}

// Using a function block argument, the
// block type will be scoped as this.
"Hello This".scopeThis { //this: String
println(length) //this.length
}
}

fun String.scopeIt(block: (String) -> Unit) {
block(this)
}

fun String.scopeThis(block: String.() -> Unit) {
block(this)
}

Output:

Hello It
10

Using this as a lambda receiver scope is useful when assigning values in a builder or for quick field access.

I used String extension functions for a simple example but there are many benefits when working with your own custom classes.

Here’s an excellent breakdown of how Kotlin’s provided scope functions differ and work under the hood: https://www.baeldung.com/kotlin/scope-functions

Type Parameters (Generics)

I don’t know that explaining Type Parameters is in the scope of this article and honestly I’m probably not the best person to teach it. I recommend either the following CodeLab or video overview for additional context.

CodeLab: https://developer.android.com/codelabs/kotlin-bootcamp-generics

Overview: https://www.youtube.com/watch?v=V-3L2TEdJXs

Passing functions as parameters

You can use Bound callable references to get access to fields and properties of an instance by prefixing with ::. There are other usages of this operator but in the case of a function it will get the invokable instance of the function itself rather than a function call. Such as:

fun main() {
runOutput(::output)
}

fun runOutput(block: (String) -> Unit) {
block("Hello, world!")
}

fun output(value: String) {
println(value)
}

Output:

Hello, world!

Read more at https://kotlinlang.org/docs/whatsnew11.html#bound-callable-references

Type Alias

A Type Alias is essentially just a way to rename types. It doesn’t add any new functionality and the only upside of using them is to clean up your code. The only downside is that using them excessively or with poor judgement can actually make your code less readable.

You can read more about how and why to use type alias here: https://www.baeldung.com/kotlin/type-aliases

I will use any excuse I can to link to baeldung.com, the quality of that site for any Java/Kotlin developer is unparalleled.

This concludes the introduction, we will now look at each part of the DSL in detail.

DSL Entry Point — implementation

The first task is to provide a clean entry point into the DSL. I wanted the call site to be as simple to use as possible while enforcing rules and allowing for some controlled variations.

/*
/Provide a function that will be called from
/the parent component, enforce the DSL, and
/return the target component.
*/
fun RBuilder.pokemonDetailView(
block: ComponentBuilderArgs<PokemonDetailProps>
) = child(PokemonDetailView::class) {
//this: RElementBuilder<P>.() -> Unit
handler(block)
}

A function like this gets created for each component that should be buildable. Its name is the element being built and the block type parameter is the prop class that the component requires which will always have an upper limit of RProps.

The two parameters for child are the class to build and a function with the signature RElementBuilder<P>.() -> Unit. The function parameter is what allows the lambda to call handler(block), because handler is an RElementBuilder Extension Function and we are scoped into it through RElementBuilder.().

Before reviewing the handler let’s take a look at the block type: ComponentBuilderArgs.

Type Alias: ComponentBuilderArgs

This was a cumbersome type with a lot going on so I lifted it up into a type alias for easier reading.

/*
/Type alias for a complex function type.
/This keeps the function signatures clean.
*/
private typealias ComponentBuilderArgs <P> = (
optionalProps: (dynamic.() -> Unit) -> Unit,
children: (RBuilder.() -> dynamic) -> Unit
) -> P

First, it accepts a generic type <P> and returns a function with multiple contained functions, each scoped for this, with a return type of the generic <P>. I didn’t see the need to break the type up any further but if I wanted to it would look like this:

private typealias ComponentBuilderArgs <P> = (
optionalProps: OptionalProps,
children: Children
) -> P

private typealias OptionalProps = (dynamic.() -> Unit) -> Unit
private typealias Children = (RBuilder.() -> dynamic) -> Unit

Understanding what’s going on with these function types now, we can connect the dots at the call site.

DSL Entry Point — usage

pokemonDetailView { optionalProps, children ->

//supply optional props
optionalProps { //this: Dynamic
optionalValue1 = "Testing, 1..2..3.."
optionalValue2 = 7357
//return Unit
}

//provide children to the target component
children { //this: RBuilder
div {
+"Hello, DSL!"
}
//return Unit
}

//end with returning the required props class
(return) PokemonDetailProps(props.match.params.id)
}

It is worth noting that Kotlin treats the last line of any function as the return value whether it is expected or not which is exploited in this DSL.

optionalProps is (dynamic.() -> Unit) -> Unit

children is (RBuilder.() -> dynamic) -> Unit

Because optionalProps and children are passed through as parameters with no expectation of a return value neither is required to be used. The following would be perfectly fine which was what this DSL was designed for.

pokemonDetailView { optionalProps, children ->
PokemonDetailProps(props.match.params.id)
}

Our function gives two named scope values corresponding to the types with an expected return of the given type parameter. In this case PokemonDetailProps as we saw previously.

With our type alias understood, we can examine the handler.

Handler — Main DSL

/*
/ Handler that gets called on a newly
/ built component, applying any and all
/ supplied modifications.
*/
fun <P : RProps> RElementBuilder<P>.handler(block: ComponentBuilderArgs<P>) {


// Provide a function that can be called
// with a dynamic scope, allowing any
// props to be passed
fun onOptionalProps(props: dynamic.() -> Unit) {

// Pass the attributes of this
// component to the caller
props(this.attrs)
}

// Provide a function that can be called
// with an RBuilder scope, which is
// the base of each component.
fun onChildren(children: RBuilder.() -> Unit) {

// Pass this component in for children
// to be passed through by the caller
children(this)
}

// Call the block with each optional
// function, and return the required props
// as defined in ComponentBuilderArgs
val result = block(::onOptionalProps, ::onChildren)

// Apply each supplied prop as required
result.asJsObject().getOwnPropertyNames().forEach {
attrs.asDynamic()[it] = result.asDynamic()[it]
}
}

We just covered this but to recap, our argument for block in this function is the verbose type alias function.

This part was a little tough for me to wrap my head around at first but as I unraveled it things started to click.

Since we call this function with values specified in the block we don’t know whether the optional props or children were provided and the handler is agnostic to it. When block() is called those functions will be provided to the call site whether they are used or not. We get the provided required props in result from the block() call and then are able to iterate over them to apply to our component’s attributes.

It may seem a bit convoluted but when you want things to work a very specific way when developing it is ok for things to get a little weird under the surface.

Now that we’ve looked at all the parts individually you can see the full code flows below.

Full DSL code and usage

pokemonDetailView { optionalProps, children ->

optionalProps {
optionalValue1 = "Testing, 1..2..3.."
optionalValue2 = 7357
}

children {
div {
+"Hello, DSL!"
}
}

PokemonDetailProps(props.match.params.id)
}

private typealias ComponentBuilderArgs <P> = (
optionalProps: (dynamic.() -> Unit) -> Unit,
children: (RBuilder.() -> dynamic) -> Unit
) -> P

fun <P : RProps> RElementBuilder<P>.handler(
block: ComponentBuilderArgs<P>
) {

fun onOptionalProps(props: dynamic.() -> Unit) {
props(this.attrs)
}

fun onChildren(children: RBuilder.() -> Unit) {
children(this)
}

val result = block(::onOptionalProps, ::onChildren)

result.asJsObject().getOwnPropertyNames().forEach {
attrs.asDynamic()[it] = result.asDynamic()[it]
}
}

data class PokemonDetailProps(var pokemonId: Int) : RProps

fun RBuilder.pokemonDetailView(
block: ComponentBuilderArgs<PokemonDetailProps>
) = child(PokemonDetailView::class) {
handler(block)
}

Wrapping Up

With this DSL written my objective is complete. Now whenever I build a new component, I will simply create a variation of the extension function like RBuilder.[MyComponent] and just use that Component’s prop type class as the generic type. This allows me to compose views rapidly and, more importantly, cleanly.

Was the time spent writing this DSL worth the time saved on the project? Maybe not. However, it did teach me a lot about how things work in Kotlin and what options I have as a developer to set up my environment to meet my needs. I hope that learning these concepts will make you feel empowered to make quality of life changes early on in a project and confident that it will pay off down the road in efficiency just as it did for me.

As always, thanks for reading and I hope you took away something valuable from this article. Remember to check back periodically as this is where I will post all of my hands-on content!

Special thanks to my friend and colleague Kyle Dahn for his help letting me bounce my ideas off of him and proofreading my articles.
https://www.linkedin.com/in/kdahn/

--

--