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

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)

Ubiratan Soares

November 15, 2019
Tweet

More Decks by Ubiratan Soares

Other Decks in Programming

Transcript

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

    View full-size slide

  2. SELECT *
    FROM accounts
    WHERE id=12345

    View full-size slide

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

    View full-size slide

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

    View full-size slide




  5. Hey!!"h1>
    Ho!Lets go!!"span>
    !"div>
    !"body>
    !"html>

    View full-size slide

  6. val tree = createHTMLDocument().html {
    body {
    h1 { +"Hey" }
    div {
    +"Ho!" span { +"Lets go!" }
    }
    }
    }
    https://github.com/Kotlin/kotlinx.html

    View full-size slide

  7. TRAILLING
    NOTATION

    View full-size slide

  8. Trailling Lamba, at your service!

    View full-size slide

  9. “A small step for the IDE, but
    a giant leap for DSLs”
    - Me, 2019

    View full-size slide

  10. INFIX
    NOTATION

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. “PLAIN ENGLISH”
    STATEMENTS

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  20. class Tinker(
    private val collected: MutableList,
    incoming: String
    ) {
    }

    View full-size slide

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

    View full-size slide

  22. class Tinker(
    private val collected: MutableList,
    incoming: String
    ) {
    init {
    collected += incoming.first().toString()
    }
    infix fun and(another: String) = Tinker(collected, another)
    }

    View full-size slide

  23. class Tinker(#$%) {
    infix fun gluedWith(option: Joiner) = TODO()
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  27. class InitialsTinker(
    private val collected: MutableList,
    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"
    }
    }

    View full-size slide

  28. sealed class Joiner
    object nothing : Joiner()
    object dots : Joiner()

    View full-size slide

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

    View full-size slide

  30. val dsl = initials from
    "Domain" and
    "Specific" and
    "Language" gluedWith
    dots //D.S.L

    View full-size slide

  31. IN THE WILD
    https://github.com/kotlintest
    KotlinTest custom matchers
    class KotlinTestDemo : StringSpec({
    "assert string length" {
    "DSL".length shouldBe 3
    }
    })

    View full-size slide

  32. OPERATOR
    OVERLOADS

    View full-size slide

  33. val list = mutableListOf(1, 2, 3)
    list += 4
    /()
    * Adds the specified [element] to this mutable collection.
    *+
    @kotlin.internal.InlineOnly
    public inline operator fun MutableCollection.plusAssign(element: T) {
    this.add(element)
    }

    View full-size slide

  34. OPERATING WITH
    java.util.Date

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  39. 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)

    View full-size slide

  40. 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)

    View full-size slide

  41. fun evaluateDates() {
    val atPast = Date() - 2.days
    val atFuture = Date() + 2.years
    }

    View full-size slide

  42. 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)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  46. 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
    }

    View full-size slide

  47. 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)
    }

    View full-size slide

  48. 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
    }

    View full-size slide

  49. 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
    }

    View full-size slide

  50. A PROBLEM LIKE
    ANDROID
    SPANNABLES

    View full-size slide

  51. 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

    View full-size slide

  52. val spanned = buildSpannedString {
    bold { append("This") }
    append(" is ")
    italic { append("fine") }
    }
    // Shiny new SpannableStringBuilder.kt

    View full-size slide

  53. ,- SomeActivity.kt
    val dsl = bold("DSLs") + " are " + italic("awesome")
    labelHello.text = dsl

    View full-size slide

  54. 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")

    View full-size slide

  55. 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")

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  62. 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)
    }
    }

    View full-size slide

  63. 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()

    View full-size slide

  64. 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")

    View full-size slide

  65. 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()

    View full-size slide

  66. LAMBDAS +
    RECEIVERS

    View full-size slide

  67. THE END OF A
    HAPPY HISTORY

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  74. 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 !!!

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  79. The secret sauce
    A Lambda extension used as the last
    argument of a high order function
    (plus trailling notation !)

    View full-size slide

  80. TYPE-SAFE
    BUILDERS

    View full-size slide

  81. fun shareMyCatalog() {
    val myCatalog: Catalog = TODO()
    println(myCatalog)
    }
    data class Movie(
    val title: String,
    val rating: Int
    )
    data class Catalog(
    val movies: List,
    val updatedAt: Date
    )

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  85. data class Catalog(
    val movies: List,
    val updatedAt: Date
    )
    class CatalogBuilder {
    var updatedAt: Date = Date()
    var movies: List = 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)
    }

    View full-size slide

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

    View full-size slide

  87. 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)
    }

    View full-size slide

  88. 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 = mutableListOf()
    fun build() = Catalog(movies, updatedAt)
    }
    fun shareMyCatalog() {
    val myCatalog = catalog {
    updatedAt = Date() - 1.days
    }
    println(myCatalog)
    }

    View full-size slide

  89. 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 = 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)
    }

    View full-size slide

  90. fun shareMyCatalog() {
    val myCatalog = catalog {
    updatedAt = Date() - 1.days
    }
    println(myCatalog)
    }
    data class Catalog(
    val movies: List,
    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 = mutableListOf()
    fun movie(block: MovieBuilder.() &' Unit) {
    movies += MovieBuilder().apply(block).build()
    }
    fun build() = Catalog(movies, updatedAt)
    }

    View full-size slide

  91. fun shareMyCatalog() {
    val myCatalog = catalog {
    updatedAt = Date() - 1.days
    movie {
    title = “TaxiDriver"
    rating = 9
    }
    }
    println(myCatalog)
    }
    data class Catalog(
    val movies: List,
    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 = mutableListOf()
    fun movie(block: MovieBuilder.() &' Unit) {
    movies += MovieBuilder().apply(block).build()
    }
    fun build() = Catalog(movies, updatedAt)
    }

    View full-size slide

  92. 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,
    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 = mutableListOf()
    fun movie(block: MovieBuilder.() &' Unit) {
    movies += MovieBuilder().apply(block).build()
    }
    fun build() = Catalog(movies, updatedAt)
    }

    View full-size slide

  93. I finally
    understand !!

    View full-size slide

  94. IN THE WILD (I)
    Kotlinx.HTML
    val tree = createHTMLDocument().html {
    body {
    h1 { +"Hey" }
    div { +"Ho!" span { +"Lets go!" } }
    }
    }
    https://github.com/Kotlin/kotlinx.html

    View full-size slide

  95. 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/

    View full-size slide

  96. 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

    View full-size slide

  97. FINAL

    REMARKS

    View full-size slide

  98. CALL TO ACTION !
    • Learn more about invoking instances in Kotlin
    • Learn about scoping with @DSLMarker
    • Design your own DSLs for fun and profit !

    View full-size slide

  99. UBIRATAN
    SOARES
    Brazilian Computer Scientist
    Senior Software Engineer @ N26
    GDE for Android and Kotlin
    @ubiratanfsoares
    ubiratansoares.dev

    View full-size slide

  100. https://speakerdeck.com/ubiratansoares

    View full-size slide

  101. THANK YOU
    Do you want to write awesome Kotlin? N26 is hiring!
    https://n26.com/en/careers

    View full-size slide