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

DSLs with Kotlin

Avatar for Dan Rusu 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)

Avatar for Dan Rusu

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