Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DSLs with Kotlin

Dan Rusu
November 16, 2018

DSLs with Kotlin

We demonstrate how to create domain specific languages (DSLs) for constructing structured data such as HTML in a type-safe manner.

We also show how DSLs can be used to enforce patterns and prevent anti-patterns (rather than constructing anything)

Dan Rusu

November 16, 2018
Tweet

More Decks by Dan Rusu

Other Decks in Programming

Transcript

  1. DSLs with Kotlin Dan Rusu S h an kh a

    Ch atterjee Nov 1 4 , 2 0 1 8
  2. FR 4 Extension Functions Kotlin Java Equivalent fun String.isCool(): Boolean

    { return this == "Shankha" } fun String?.isNullOrEmpty(): Boolean { return this == null || this.isEmpty() } … if ("Dan".isCool()) print("Dan is cool") public class StringUtils { public static boolean isCool(String receiver) { return receiver.equals("Shankha"); } } … if(StringUtils.isCool("Dan")) System.out.print("Dan is cool");
  3. FR 6 Lambdas /** Try with resources */ inline fun

    using(resource: AutoCloseable, body: () -> Unit) { try { body() } finally { resource.close() } } … var serverPath: String val resource = BufferedReader(FileReader("config.txt")) using(resource) { serverPath = resource.readLine() }
  4. FR Lambda with Receiver 8 fun buildString(compose: StringBuilder.() -> Unit):

    String { val builder = StringBuilder() builder.compose() return builder.toString() } fun logError(severity: String, message: String) { val error = buildString { append("Severity: ") append(severity) append("\n Message: ") append(message) } log(error) }
  5. FR DSLs for type-safe builders • Java builders are used

    to build objects • Imperative, not declarative • Can use Kotlin DSLs to build nested object structures • Declarative • Enforcing the hierarchy at compile time, cannot do the wrong thing • Example – HTML builder 10
  6. FR 11 html { head { title {+"HTML encoding with

    Kotlin"} } body { p {+"this format can be used as an alternative markup to XML"} a(href = "http://kotlinlang.org") {+"Kotlin"} p { +"Paragraph with some linked" a(href = "http://kotlinlang.org") {+"Kotlin"} +"text" } p { for (person in people) +person.name } } } DSL for HTML
  7. FR 12 new HTML.Builder() .head( new Head.Builder() .title("The title") .build()

    ) .body( new Body.Builder() .paragraph( new Paragraph.Builder() .text("this format can be used as an alternative markup to XML") .build() ) .link("http://kotlinlang.org", "Kotlin") .paragraph( new Paragraph.Builder() .text("Paragraph with some linked") .link("http://kotlinlang.org", "Kotlin") .text("text") .build() ) .paragraph(new Paragraph.Builder() .modify( paragraphBuilder -> { for (person: people) { paragraphBuilder.text(person.getName()) } }) .build() ) .build() ) .build(); The Noisy Java Alternative
  8. FR 13 html { head { title {+"HTML encoding with

    Kotlin"} body { } } body { head { } p {+"this format can be used as an alternative markup to XML"} // content generated by p { for (person in people) +person.name } } } Incorrect HTML does not compile
  9. FR @DslMarker annotation class HtmlDSL @HtmlDSL interface Element { fun

    render(builder: StringBuilder, indent: String) } class TextElement(val text: String) : Element { override fun render(builder: StringBuilder, indent: String) { … } } abstract class Tag(val name: String) : Element { val children = arrayListOf<Element>() val attributes = hashMapOf<String, String>() protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T { tag.init() children.add(tag) return tag } } 14 How did we achieve such greatness?
  10. FR abstract class TagWithText(name: String) : Tag(name) { operator fun

    String.unaryPlus() { children.add(TextElement(this)) } } class HTML : TagWithText("html") { fun head(init: Head.() -> Unit) = initTag(Head(), init) fun body(init: Body.() -> Unit) = initTag(Body(), init) } class Head : TagWithText("head") { fun title(init: Title.() -> Unit) = initTag(Title(), init) } class Title : TagWithText("title") 15 Create class for each tag
  11. FR abstract class BodyTag(name: String) : TagWithText(name) { fun p(init:

    P.() -> Unit) = initTag(P(), init) fun a(href: String, init: A.() -> Unit) { val a = initTag(A(), init) a.href = href } } class Body : BodyTag("body") class P : BodyTag("p") class A : BodyTag("a") { var href: String get() = attributes["href"] set(value) { attributes["href"] = value } } 16 Create class for each tag
  12. FR fun html(init: HTML.() -> Unit): HTML { val html

    = HTML() html.init() return html } 17 Include a top-level function
  13. FR Bank Account Goals • Developers aren’t perfect and tests

    aren’t comprehensive • Can we enforce correct use of patterns? • Guarantee thread safety at compile time: • Must hold balance lock when accessing balance • Must hold address lock when accessing the address • Must acquire locks in the same order to prevent deadlocks • When holding both locks, address lock must be acquired last • When acquiring address lock first, the balance lock cannot be acquired 19
  14. FR 20 DSLs for Pattern Enforcement class BankAccount(val id: Long,

    streetNumber: Int, streetName: String, city: String) { private val transactionManager = TransactionManager(BankAccountData(streetNumber, streetName, city)) fun withdraw(amount: Long) { transactionManager.inTransaction { withBalanceLock { require(amount <= balanceInPennies) { "Insufficient funds" } balanceInPennies -= amount if (amount > 1_000_000_00) { withAddressLock { sendSecurityNotification(amount) } } } } } fun updateAddress(number: Int, street: String, city: String) { transactionManager.inTransaction { withAddressLock { streetNumber = number … } } } }
  15. FR 21 Incorrect Attempt class BankAccount(val id: Long, streetNumber: Int,

    streetName: String, city: String) { private val transactionManager = TransactionManager(BankAccountData(streetNumber, streetName, city)) fun withdraw(amount: Long) { // Good transactionManager.inTransaction { withBalanceLock { balanceInPennies -= amount if (amount > 1_000_000_00) { withAddressLock { sendSecurityNotification(amount) } } } } } fun deposit(amount: Long) { // Bad balanceInPennies += amount // Balance only available after acquiring the balance lock withBalanceLock { … } // Only available within a transaction transactionManager.inTransaction { withAddressLock { withBalanceLock { … } // Invalid attempt to acquire locks in wrong order } } } }
  16. FR 22 class TransactionManager<T: Any>(private val scope: T) { private

    var isActive = object: ThreadLocal<Boolean>() { override fun initialValue(): Boolean = false } fun inTransaction(body: T.() -> Unit) { check(!isActive.get()) { "Nested transactions are not allowed!" } try { isActive.set(true) scope.body() } finally { isActive.set(false) } } } Force Single Entry Point
  17. FR 23 @DslMarker annotation class BankDSL @BankDSL class BankAccountData(streetNumber: Int,

    streetName: String, city: String) { private val balanceLock = ReentrantLock() private val addressLock = ReentrantLock() private val addressContext = BankAccountAddress(streetNumber, streetName, city) private val moneyContext = BankAccountBalance(addressLock, addressContext) fun withBalanceLock(body: BankAccountBalance.() -> Unit) { balanceLock.withLock { moneyContext.body() } } fun withAddressLock(body: BankAccountAddress.() -> Unit) { addressLock.withLock { addressContext.body() } } } Expose Starting Contexts
  18. FR 24 @BankDSL class BankAccountBalance(private val addressLock: Lock, private val

    addressContext: BankAccountAddress) { var balanceInPennies = 0L fun withAddressLock(body: BankAccountAddress.() -> Unit) { addressLock.withLock { addressContext.body() } } } @BankDSL class BankAccountAddress(var streetNumber: Int, var streetName: String, var cityName: String) { fun sendSecurityNotification(amount: Long) { // Send receipt to address for a large withdrawals } } Implement Individual Contexts
  19. FR • Gradle Kotlin DSL • Define build scripts in

    a type-safe way using autocomplete • Ktor • High performance web application framework utilizing Kotlin coroutines • Anko • Android application development • TornadoFX • Build Java FX applications with ease 26