DSL - The Kotlin Way

DSL - The Kotlin Way

Supporting slides from my talk about internal DSLs using Kotlin features. This is v3.0 of this talk!

Delivered in the following conferences / events

- DevFest Bucharest (November / 2019)

D4b7a3e2ed10f86e0b52498713ba2601?s=128

Ubiratan Soares

November 15, 2019
Tweet

Transcript

  1. DSLs IN A KOTLIN WAY Ubiratan Soares November / 2019

  2. None
  3. None
  4. SELECT * FROM accounts WHERE id=12345

  5. SELECT * FROM accounts WHERE id=12345 " "

  6. String sql = SQLiteQueryBuilder .select("*") .from("accounts") .where("id=12345") .toString(); https://github.com/alexfu/SQLiteQueryBuilder

  7. <!DOCTYPE html> <html> <body> <h1>Hey!!"h1> <div>Ho!<span>Lets go!!"span> !"div> !"body> !"html>

  8. val tree = createHTMLDocument().html { body { h1 { +"Hey"

    } div { +"Ho!" span { +"Lets go!" } } } } https://github.com/Kotlin/kotlinx.html
  9. None
  10. None
  11. None
  12. None
  13. TRAILLING NOTATION

  14. None
  15. None
  16. Trailling Lamba, at your service!

  17. “A small step for the IDE, but a giant leap

    for DSLs” - Me, 2019
  18. INFIX NOTATION

  19. class Extractor { fun firstLetter(target: String) = target[0].toString() } val

    k = Extractor().firstLetter("Kotlin")
  20. class Extractor { infix fun firstLetter(target: String) = target[0].toString() }

    val k = Extractor() firstLetter "Kotlin" NO PUNCTUATION!
  21. class Extractor { infix fun firstLetter(target: String) = target[0].toString() }

    val k = Extractor() firstLetter "Kotlin" NO PARENTHESIS
  22. object Extract { infix fun firstLetterOf(target: String) = target[0].toString() }

    val first = Extract firstLetterOf “Awesome"
  23. object extract { infix fun firstLetterOf(target: String) = target[0].toString() }

    val first = extract firstLetterOf “Awesome"
  24. None
  25. “PLAIN ENGLISH” STATEMENTS

  26. initials from “Kotlin" and "Rocks" gluedWith dots !" K.R

  27. object initials { infix fun from(target: String) = Tinker( collected

    = mutableListOf(), incoming = target ) }
  28. object initials { infix fun from(target: String) = Tinker( collected

    = mutableListOf(), incoming = target ) }
  29. class Tinker( private val collected: MutableList<String>, incoming: String ) {

    }
  30. class Tinker( private val collected: MutableList<String>, incoming: String ) {

    init { collected += incoming.first().toString() } }
  31. class Tinker( private val collected: MutableList<String>, incoming: String ) {

    init { collected += incoming.first().toString() } infix fun and(another: String) = Tinker(collected, another) }
  32. class Tinker(#$%) { infix fun gluedWith(option: Joiner) = TODO() }

  33. class Tinker(#$%) { infix fun gluedWith(option: Joiner) = collected.reduce {

    previous, letter &' } }
  34. class Tinker(#$%) { infix fun gluedWith(option: Joiner) = collected.reduce {

    previous, letter &' val next = when (option) { is nothing &' letter is dots &' ".$letter" } } }
  35. class Tinker(#$%) { infix fun gluedWith(option: Joiner) = collected.reduce {

    previous, letter &' val next = when (option) { is nothing &' letter is dots &' ".$letter" } "$previous$next" } }
  36. class InitialsTinker( private val collected: MutableList<String>, incoming: String ) {

    init { collected += incoming.first().toString() } infix fun and(another: String) = InitialsTinker(collected, another) infix fun gluedWith(option: Joiner) = collected.reduce { previous, letter &' val next = when (option) { is nothing &' letter is dots &' ".$letter" } "$previous$next" } }
  37. sealed class Joiner object nothing : Joiner() object dots :

    Joiner()
  38. val dsl = initials from "Domain" and "Specific" and "Language"

    gluedWith nothing //DSL
  39. val dsl = initials from "Domain" and "Specific" and "Language"

    gluedWith dots //D.S.L
  40. IN THE WILD https://github.com/kotlintest KotlinTest custom matchers class KotlinTestDemo :

    StringSpec({ "assert string length" { "DSL".length shouldBe 3 } })
  41. OPERATOR OVERLOADS

  42. val list = mutableListOf(1, 2, 3) list += 4 /()

    * Adds the specified [element] to this mutable collection. *+ @kotlin.internal.InlineOnly public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) { this.add(element) }
  43. OPERATING WITH java.util.Date

  44. fun addMonths(target: Date, months: Int): Date { // TODO }

  45. fun addMonths(target: Date, months: Int): Date { val calendar =

    Calendar.getInstance() }
  46. fun addMonths(target: Date, months: Int): Date { val calendar =

    Calendar.getInstance() calendar.time = target calendar.add(Calendar.MONTH, months) }
  47. fun addMonths(target: Date, months: Int): Date { val calendar =

    Calendar.getInstance() calendar.time = target calendar.add(Calendar.MONTH, months) return calendar.time }
  48. fun addMonths(target: Date, months: Int): Date { val calendar =

    Calendar.getInstance() calendar.time = target calendar.add(Calendar.MONTH, months) return calendar.time } val twoMonthsLater = addMonths(Date(), 2)
  49. fun subtractDays(target: Date, days: Int): Date { val calendar =

    Calendar.getInstance() calendar.time = target calendar.add(Calendar.DAY_OF_YEAR, -days) return calendar.time } val oneWeekAgo = subtractDays(Date(), 7)
  50. fun evaluateDates() { val atPast = Date() - 2.days val

    atFuture = Date() + 2.years }
  51. data class DateIncrement( val dateField : Int, val amount: Int

    ) val Int.days: DateIncrement get() = DateIncrement(Calendar.DAY_OF_YEAR, this) val Int.months: DateIncrement get() = DateIncrement(Calendar.MONTH, this) val Int.years: DateIncrement get() = DateIncrement(Calendar.YEAR, this)
  52. fun Date.applyIncrement( increment: DateIncrement, operation: IncrementOperation): Date = with(Calendar.getInstance()) {

    // TODO }
  53. fun Date.applyIncrement( increment: DateIncrement, operation: IncrementOperation): Date = with(Calendar.getInstance()) {

    val (field, amount) = increment }
  54. fun Date.applyIncrement( increment: DateIncrement, operation: IncrementOperation): Date = with(Calendar.getInstance()) {

    val (field, amount) = increment val translatedAmount = when(operation) { DECREASE &' -amount INCREASE &' amount } }
  55. fun Date.applyIncrement( increment: DateIncrement, operation: IncrementOperation): Date = with(Calendar.getInstance()) {

    val (field, amount) = increment val translatedAmount = when(operation) { DECREASE &' -amount INCREASE &' amount } time = this@applyIncrement }
  56. fun Date.applyIncrement( increment: DateIncrement, operation: IncrementOperation): Date = with(Calendar.getInstance()) {

    val (field, amount) = increment val translatedAmount = when(operation) { DECREASE &' -amount INCREASE &' amount } time = this@applyIncrement add(field, translatedAmount) }
  57. fun Date.applyIncrement( increment: DateIncrement, operation: IncrementOperation): Date = with(Calendar.getInstance()) {

    val (field, amount) = increment val translatedAmount = when(operation) { DECREASE &' -amount INCREASE &' amount } time = this@applyIncrement add(field, translatedAmount) time }
  58. operator fun Date.plus(increment: DateIncrement): Date = applyIncrement(increment, INCREASE) operator fun

    Date.minus(increment: DateIncrement): Date = applyIncrement(increment, DECREASE) val atPast = Date() - 2.days val atFuture = Date() + 2.years enum class IncrementOperation { INCREASE, DECREASE }
  59. A PROBLEM LIKE ANDROID SPANNABLES

  60. val spannable = SpannableString("Pain is not enough!") spannable.setSpan( ForegroundColorSpan(Color.RED), 0,

    3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) spannable.setSpan( StyleSpan(BOLD), 12, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) // plain old SpannableString.java
  61. None
  62. val spanned = buildSpannedString { bold { append("This") } append("

    is ") italic { append("fine") } } // Shiny new SpannableStringBuilder.kt
  63. ,- SomeActivity.kt val dsl = bold("DSLs") + " are "

    + italic("awesome") labelHello.text = dsl
  64. fun bold(given: CharSequence) = TODO() span(given, StyleSpan(Typeface.BOLD)) fun italic(given: CharSequence)

    = TODO() span(given, StyleSpan(Typeface.ITALIC)) val dsl = bold("DSLs") + " are " + italic("awesome")
  65. fun bold(given: CharSequence) = span(given, StyleSpan(Typeface.BOLD)) fun italic(given: CharSequence) =

    span(given, StyleSpan(Typeface.ITALIC)) val dsl = bold("DSLs") + " are " + italic("awesome")
  66. fun span(target: CharSequence, style: Any) : SpannableString =

  67. fun span(target: CharSequence, style: Any) : SpannableString = when (target)

    { }
  68. fun span(target: CharSequence, style: Any) : SpannableString = when (target)

    { is String &' SpannableString(target) }
  69. fun span(target: CharSequence, style: Any) : SpannableString = when (target)

    { is String &' SpannableString(target) is SpannableString &' target }
  70. fun span(target: CharSequence, style: Any) : SpannableString = when (target)

    { is String &' SpannableString(target) is SpannableString &' target else &' throw CannotBeSpanned }
  71. fun span(target: CharSequence, style: Any) : SpannableString = when (target)

    { is String &' SpannableString(target) is SpannableString &' target else &' throw CannotBeSpanned }.let { spannable &' }
  72. fun span(target: CharSequence, style: Any) : SpannableString = when (target)

    { is String &' SpannableString(target) is SpannableString &' target else &' throw CannotBeSpanned }.let { spannable &' spannable.apply { setSpan(style, 0, length, SPAN_EXCLUSIVE_EXCLUSIVE) } }
  73. fun span(target: CharSequence, style: Any) : SpannableString = when (target)

    { is String &' SpannableString(target) is SpannableString &' target else &' throw CannotBeSpanned }.let { spannable &' spannable.apply { setSpan(style, 0, length, SPAN_EXCLUSIVE_EXCLUSIVE) } } object CannotBeSpanned : IllegalArgumentException()
  74. operator fun SpannableString.plus(s: SpannableString) = SpannableString(TextUtils.concat(this, s)) operator fun SpannableString.plus(s:

    String) = SpannableString(TextUtils.concat(this, s)) val spanned = bold("DSLs") + " are " + italic("awesome")
  75. None
  76. IN THE WILD https://github.com/ReactiveX/RxKotlin RxKotlin CompositeDisposable enhancement private val composite

    = CompositeDisposable() private val stream = Observable.just(1, 2, 3) composite += stream.subscribe()
  77. LAMBDAS + RECEIVERS

  78. THE END OF A HAPPY HISTORY

  79. data class Person( var name: String, var married: Boolean )

  80. data class Person( var name: String, var married: Boolean )

    fun Person.freeAgain() { this.married = false }
  81. data class Person( var name: String, var married: Boolean )

    fun Person.freeAgain() { this.married = false } val changeToSingle = Person./freeAgain
  82. fun theEndOfHistory() { val unhappy = Person("Alice Rodriguez", true) val

    optimistic = Person("Bob Rodriguez", true) }
  83. fun theEndOfHistory() { val unhappy = Person("Alice Rodriguez", true) val

    optimistic = Person("Bob Rodriguez", true) unhappy.freeAgain() println(unhappy) }
  84. fun theEndOfHistory() { val unhappy = Person("Alice Rodriguez", true) val

    optimistic = Person("Bob Rodriguez", true) unhappy.freeAgain() println(unhappy) changeToSingle(optimistic) println(optimistic) }
  85. val changeToSingle = Person./freeAgain val unhappy = Person("Alice Rodriguez", true)

    val optimistic = Person("Bob Rodriguez", true) marriedNoMore(unhappy) changeToSingle(optimistic) val marriedNoMore : Person.() &' Unit = { this.married = false } Called in the same way !!!
  86. fun theTrueEndForAlice(block : Person.() &' Unit) { val newPerson =

    Person("Alice", false) newPerson.block() println(newPerson) }
  87. fun theTrueEndForAlice(block : Person.() &' Unit) { val newPerson =

    Person("Alice", false) newPerson.block() println(newPerson) } fun main() { theTrueEndForAlice { name = "Alice Springs" } } Trailling Notation !!!!
  88. fun theTrueEndForAlice(block : Person.() &' Unit) { println(Person("Alice", false).apply(block)) }

    fun main() { theTrueEndForAlice { name = "Alice Springs" } }
  89. fun theTrueEndForAlice(block : Person.() &' Unit) { println(Person("Alice", false).apply(block)) }

    fun main() { theTrueEndForAlice { name = "Alice Springs" } }
  90. The secret sauce A Lambda extension used as the last

    argument of a high order function (plus trailling notation !)
  91. TYPE-SAFE BUILDERS

  92. fun shareMyCatalog() { val myCatalog: Catalog = TODO() println(myCatalog) }

    data class Movie( val title: String, val rating: Int ) data class Catalog( val movies: List<Movie>, val updatedAt: Date )
  93. data class Catalog( val movies: List<Movie>, val updatedAt: Date )

    fun shareMyCatalog() { val myCatalog: Catalog = TODO() println(myCatalog) }
  94. data class Catalog( val movies: List<Movie>, val updatedAt: Date )

    class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun build() = Catalog(movies, updatedAt) } fun shareMyCatalog() { val myCatalog: Catalog = TODO() println(myCatalog) }
  95. data class Catalog( val movies: List<Movie>, val updatedAt: Date )

    class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun build() = Catalog(movies, updatedAt) } fun catalog(block: CatalogBuilder.() &' Unit) = CatalogBuilder().apply(block).build() fun shareMyCatalog() { val myCatalog = catalog { } println(myCatalog) }
  96. data class Catalog( val movies: List<Movie>, val updatedAt: Date )

    class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun build() = Catalog(movies, updatedAt) } fun catalog(block: CatalogBuilder.() &' Unit) = CatalogBuilder().apply(block).build() fun shareMyCatalog() { val myCatalog = catalog { updatedAt = Date() - 1.days } println(myCatalog) }
  97. data class Movie( val title: String, val rating: Int )

    fun shareMyCatalog() { val myCatalog = catalog { updatedAt = Date() - 1.days } println(myCatalog) }
  98. data class Movie( val title: String, val rating: Int )

    class MovieBuilder { var title: String = "" var rating: Int = 0 fun build() = Movie(title, rating) } fun shareMyCatalog() { val myCatalog = catalog { updatedAt = Date() - 1.days } println(myCatalog) }
  99. data class Movie( val title: String, val rating: Int )

    class MovieBuilder { var title: String = "" var rating: Int = 0 fun build() = Movie(title, rating) } class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun build() = Catalog(movies, updatedAt) } fun shareMyCatalog() { val myCatalog = catalog { updatedAt = Date() - 1.days } println(myCatalog) }
  100. data class Movie( val title: String, val rating: Int )

    class MovieBuilder { var title: String = "" var rating: Int = 0 fun build() = Movie(title, rating) } class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun movie(block: MovieBuilder.() &' Unit) { val movie = MovieBuilder().apply(block).build() movies += movie } fun build() = Catalog(movies, updatedAt) } fun shareMyCatalog() { val myCatalog = catalog { updatedAt = Date() - 1.days } println(myCatalog) }
  101. fun shareMyCatalog() { val myCatalog = catalog { updatedAt =

    Date() - 1.days } println(myCatalog) } data class Catalog( val movies: List<Movie>, val updatedAt: Date ) data class Movie( val title: String, val rating: Int ) class MovieBuilder { var title: String = "" var rating: Int = 0 fun build() = Movie(title, rating) } class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun movie(block: MovieBuilder.() &' Unit) { movies += MovieBuilder().apply(block).build() } fun build() = Catalog(movies, updatedAt) }
  102. fun shareMyCatalog() { val myCatalog = catalog { updatedAt =

    Date() - 1.days movie { title = “TaxiDriver" rating = 9 } } println(myCatalog) } data class Catalog( val movies: List<Movie>, val updatedAt: Date ) data class Movie( val title: String, val rating: Int ) class MovieBuilder { var title: String = "" var rating: Int = 0 fun build() = Movie(title, rating) } class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun movie(block: MovieBuilder.() &' Unit) { movies += MovieBuilder().apply(block).build() } fun build() = Catalog(movies, updatedAt) }
  103. fun shareMyCatalog() { val myCatalog = catalog { updatedAt =

    Date() - 1.days movie { title = “Taxi Driver" rating = 9 } movie { title = "The GoodFellas" rating = 10 } } println(myCatalog) } data class Catalog( val movies: List<Movie>, val updatedAt: Date ) data class Movie( val title: String, val rating: Int ) class MovieBuilder { var title: String = "" var rating: Int = 0 fun build() = Movie(title, rating) } class CatalogBuilder { var updatedAt: Date = Date() var movies: List<Movie> = mutableListOf() fun movie(block: MovieBuilder.() &' Unit) { movies += MovieBuilder().apply(block).build() } fun build() = Catalog(movies, updatedAt) }
  104. I finally understand !!

  105. IN THE WILD (I) Kotlinx.HTML val tree = createHTMLDocument().html {

    body { h1 { +"Hey" } div { +"Ho!" span { +"Lets go!" } } } } https://github.com/Kotlin/kotlinx.html
  106. IN THE WILD (II) Ktor val server = embeddedServer(Netty, port

    = 8080) { routing { get("/") { call.respondText("Hello World!", ContentType.Text.Plain) } } } server.start(wait = true) https://ktor.io/
  107. IN THE WILD (III) Spek object SetFeature: Spek({ Feature("Set") {

    Scenario("adding items") { When("adding foo") { set.add(“foo") } Then("it should have a size of 1") { assertEquals(1, set.size) } } } https://spekframework.org
  108. None
  109. FINAL
 REMARKS

  110. Kotlin + DSLs

  111. CALL TO ACTION ! • Learn more about invoking instances

    in Kotlin • Learn about scoping with @DSLMarker • Design your own DSLs for fun and profit !
  112. UBIRATAN SOARES Brazilian Computer Scientist Senior Software Engineer @ N26

    GDE for Android and Kotlin @ubiratanfsoares ubiratansoares.dev
  113. https://speakerdeck.com/ubiratansoares

  114. THANK YOU Do you want to write awesome Kotlin? N26

    is hiring! https://n26.com/en/careers