Fantastic

DSLs

and where to find them


Sergey Tselovalnikov
http://serce.me
GitHub @SerCeMan
Twitter @SerCeMan

Warning

What is DSL

A computer programming language of limited expressiveness focused on a particular domain
Martin Fowler
A language (or a set of abstractions) that's built to deal with a specific domain

Calling conventions


fun dotimes(n: Int, f: () -> Unit) {
    for (i in 0..n-1) {
        f()
    }
}
            

dotimes(5, {
    println("Hello, Kotlin!")
})
            

dotimes(5) {
    println("Hello, Kotlin!")
}
            

do5times {
    println("Hello, Kotlin!")
}
            

Extension functions


fun String.removeSpaces(): String {
  return this.filter { c -> c != ' ' }
}

print("Hi ! , ext".removeSpaces()) // "Hi!,ext"

                        

// from stdlib
public inline fun String.filter(
  predicate: (Char) -> Boolean): String {
  val destination = StringBuilder()
  for (index in 0..length - 1) {
    val element = get(index)
    if (predicate(element))
      destination.append(element)
  }
  return destination.toString()
}

                        

static String removeSpaces(String $receiver) {
  StringBuilder sb = new StringBuilder();
  int len = $receiver.length();
  for (int i = 0; i < len; i++) {
    char c = $receiver.charAt(i);
    if (c != ' ') {
      sb.append(c);
    }
  }
  return sb.toString();
}

                        

droid DSL


interface Drоid {
  val peopleAround: Boolean
  val gun: Gun

  fun fire(gun: Gun)
  fun moveLeft()
  fun moveRight()
}

                        

fun on(cmd: String, f: Droid.() -> Unit) {
// ...
  droid.f()
// ...
}

//

on("back") {
  moveLeft()
  if (peopleAround) {
    fire(gun)
  }
}
                        

static void on(String cmd,
               Function1<Droid, Unit> f) {
  // ...
  f.invoke(droid);
  // ...
}

public static void main() {
  on("back", new Function1<Droid, Unit>() {
    public Unit invoke(Droid droid) {
      droid.moveLeft();
      if (droid.getPeopleAround()) {
        droid.fire(droid.getGun());
      }
      return Unit.INSTANCE;
    }
  });
}

                        

HTML

github.com/Kotlin/kotlinx.html

val list = listOf("Kotlin", "is", "awesome")
val result: HTML =
  html {
    head {
      title { +"HTML DSL in Kotlin" }
    }
    body {
      p {
        +"a line about Kotlin"
        +"another line"
      }
      a(href = "http://jetbrains.com/kotlin") {
        +"Kotlin"
      }
      p {
        +"Kotlin is:"
        ul {
          for (arg in list)
            li { +arg }
        }
      }
    }
  }
println(result)

IDE

Teamcity DSL

blog.jetbrains.com/teamcity/2016/11/kotlin-configuration-scripts-an-introduction/

object Project : Project({
  uuid = "my_project_id"
  extId = "ExampleOfDSL"
  name = "Example of DSL"

  val vcsRoot = GitVcsRoot({
    uuid = "my_vcs_root_id"
    url = "..."
  })
  buildType {
    name = "Build"
    buildNumberPattern = "%build.counter%"
    vcs {
      root(vcsRoot)
    }
    maven {
      goals = "clean test"
    }
  }
})

Gradle Kotlin

github.com/gradle/gradle-script-kotlin

apply {
    plugin("com.android.application")
    plugin("kotlin-android")
}

android {
    buildToolsVersion("25.0.0")
    compileSdkVersion(23)

    defaultConfig {
        minSdkVersion(15)

        applicationId = "com.example.kotlingradle"
    }
}

dependencies {
    compile("com.android.support:appcompat-v7:23.4.0")
    compile(kotlinModule("stdlib"))
}

Spek

spekframework.org

object SimpleSpec: Spek({
    describe("a calculator") {
        val calculator = SampleCalculator()

        on("addition") {
            val sum = calculator.sum(2, 4)

            it("should return the result of adding") {
                assertEquals(6, sum)
            }
        }

        on("subtraction") {
            val subtract = calculator.subtract(4, 2)

            it("should return the result of subtracting") {
                assertEquals(2, subtract)
            }
        }
    }
})
            

Anko

github.com/Kotlin/anko

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    verticalLayout {
        padding = dip(30)
        editText {
            hint = "Name"
            textSize = 24f
        }
        editText {
            hint = "Password"
            textSize = 24f
        }
        button("Login") {
            textSize = 26f
        }
    }
}
            

Domain

Data classes


data class Transaction(val payment: Payment,
                       val parts: Parts)
data class Payment(val currency: String,
                   val amount: Int)
data class Parts(val from: Person,
                 val to: Person)
data class Person(val id: Int, val name: String)

                        

// update 1
val stansTrs = Transaction(trs.payment, Parts(
  Person(trs.parts.from.id, "john"),
  trs.parts.to)
)

                        

// create
val trs = Transaction(
  Payment("AUD", 15),
  Parts(
    Person(0, "alex"),
    Person(1, "ben")
  )
)

                        

// update 2
val stansTrs2 = trs.copy(
  parts = trs.parts.copy(
    from = trs.parts.from.copy(
      name = "john"
    )
  )
)


                        

Data classes


// update 2
val stansTrs2 = trs.copy(
  parts = trs.parts.copy(
    from = trs.parts.from.copy(
      person = trs.parts.from.person.copy(
        parts = trs.parts.from.person.parts.copy(
          from = trs.parts.from.person.parts.from.copy(
            person = trs.parts.from.person.parts.from.person.copy(
              parts = trs.parts.from.person.parts.from.person.parts.copy(
                from = trs.parts.from.person.parts.from.person.parts.from.copy(
                  person = trs.parts.from.person.parts.from.person.parts.from.person.copy(
                    parts = trs.parts.from.person.parts.from.person.parts.from.person.parts.copy(
                      from = trs.parts.from.person.parts.from.person.parts.from.person.parts.from.copy(
                        name = "jonh"
                      )
                    )
                  )
                )
              )
            )
          )
        )
      )
    )
  )
)
            

Clojure way


; create
(def ts {:payment {:currency "AUD"
                   :amount   15}
         :parts   {:from {:id   0
                          :name "alex"}
                   :to   {:id   1
                          :name "ben"}}})


                        

; update
(def ts2 (assoc-in ts [:parts :from :name] "john"))
                        

Persistent Data Structures


// create
val tran = pArrayMap(
  "payment" to pArrayMap(
    "currency" to "AUD",
    "amount" to 15
  ),
  "parts" to pArrayMap(
    "from" to pArrayMap(
      "id" to 0,
      "name" to "alex"
    ),
    "to" to pArrayMap(
      "id" to 1,
      "name" to "ben"
    )
  )
)

                        

// update
val path = listOf("parts", "from", "name")
val tran2 = tran.pUpdate(path, "john")



                        
                        

Extension function on generic types


fun Collection<Int>.toIntArray(): IntArray {
  val result = IntArray(size)
  var index = 0
  for (element in this)
    result[index++] = element
  return result
}
            

Delegated Properties


import kotlin.reflect.KProperty

class Niffler

class MyWorld {
  val niffler: Niffler? by Provider()
  val creature: Niffler? by Provider()
}

class Provider {
  operator fun getValue(ref: MyWorld,
                        prop: KProperty<*>
  ): Niffler? {
    return when (prop.name) {
      "niffler" -> Niffler()
      else -> null
    }
  }
}

println(MyWorld().niffler) // Niffler@7c98a2ab
println(MyWorld().creature) // null


                        

static class MyWorld {
  public static KProperty[] $$delegProps =
    new KProperty[] {
      Reflection.property1(/*🔮*/)
    };
  private final Provider niffler$;

  public MyWorld() {
    this.niffler$ = new Provider();
  }

  public Creature getNiffler() {
    return niffler$.
      getValue(this, $$delegProps[0]);
  }
}

                        

Kotlin DSL


interface Transaction
val <F> Cursor<Transaction, F>.payment by Node<Payment>()
val <F> Cursor<Transaction, F>.parts by Node<Parts>()

interface Payment
val <F> Cursor<Payment, F>.currency by Leaf<String>()
val <F> Cursor<Payment, F>.amount by Leaf<Int>()

interface Parts
val <F> Cursor<Parts, F>.to by Node<Person>()
val <F> Cursor<Parts, F>.from by Node<Person>()

interface Person
val <F> Cursor<Person, F>.id by Leaf<Int>()
val <F> Cursor<Person, F>.name by Leaf<String>()
                        

// create
val trans = domain<Transaction> {
  (payment) {
    currency.set("AUD")
    amount.set(15)
  }
  (parts) {
    (from) {
      id.set(0)
      name.set("alex")
    }
    (to) {
      id.set(1)
      name.set("ben")
    }
  }
}
                        

// update
val trans2 = trans.cursor.parts.from.update {
  name.set("john")
}
                        

Conclusions

  • Kotlin provides many unique features to build DSLs easily
  • DSLs in Kotlin work best as configuration API
  • They can be a powerful abstraction over untyped data structures

Warning

  • Most of the time a plain code is better than DSL
  • Provide a way to extend and bypass your DSL


Don't use it for evil

class Ternary<T>(val expr: Boolean, val then: T) {
  operator fun div(elsе: T): T = if (expr) then else elsе
}

operator fun <T> Boolean.mod(a: T): Ternary<T> = Ternary(this, a)

val result = (5 == 2 + 3) % 1 / 2 // 1
            
gist.github.com/naixx/9d94c1498c4d45ffda3a

Thank you

Some resources: