본문 바로가기
프로그래밍 놀이터/Kotlin, Coroutine

[Kotlin Tutorial] DSL construction - Chap 11.

by 돼지왕 왕돼지 2017. 9. 14.
반응형

 [Kotlin Tutorial] DSL construction - Chap 11.


참조 : Kotlin in action

@dslmarker, Abstraction, Android UI, anko, API, api clean, api design pattern, api to dsl, apply, apply vs with, assertion, building html with internal dsl, building structured apis, buildstring, call context, chained method call, chaning infix calls, command-query api, compile time, concept, convention, Count, country customer, createHTML, DATABASE, debugging, declarative language, defining extensions on primitive type, dependency, Domain Specific Language, DSL, dsl grammar, dsl naming convention, dsl 장점, dynamic builder, Engine, exposed framework, extension function, extension function tyep, extension function type, external dsl, external template, extract function, file, Framework, function type, function type parameter, Function1, functional interface, functional type, general purpose language, Get, GPL, gradle, Grammer, Groovy, GroupBy, html builder, igher-order function, imperative language, index operator, infix, infix calls, inline, Interface, internal dsl, internal dsl for sql, internal dsls, Invoke, invoke convention, invoke method, Join, kotiln basics, Kotlin, kotlin builders, kotlin dsl, kotlin framework, kotlin tutorial, kotlintest, kotlinx.html, lambda invoke, lambda syntax shortcut, lambda this, lambda with receiver, lambda 의 this, lambda 쪼개기, lambda 호출, lambdas, lambdas with receiver, lambdas with receivers, learning curve, lib, Limit, markup language, member extension, member extension function, Method, multiple method call, Name, name-resolution rule, named object, nesting lambda, object, objects callable as functions, operator overloading, orderby, primitive type extension, qualifier, receiver object, receiver type, receivers, Regular Expression, reuse, Scope, selectall, should, singleton object, slice, SQL, SqlExpressionBuilder, static type, string literal, structure, structure of dsls, syntax, This, this@functionName, this@label, type-safe, Unit, With, [Kotlin Tutorial] DSL construction - Chap 11., 가독성, 유지보수성, 읽기 좋은 것, 접근제한, 최소한의 code, 코틀린, 코틀린 강좌, 코틀린 기초 강좌


11.1. From APIs to DSLs


-

DSL 을 작성하기 전에 생각해봐야 할 것이 있다. 우리의 (Kotlin?) 궁극적 목표는 가독성과 유지보수성을 최대로 늘리는 것.

그것은 곧 좋은 API 를 설계하는 것으로 이어진다.


그렇다면 API 가 clean 하다는 것은 무슨 의미일까?

1. 사용자가 읽기 좋은 것. 그것은 name 과 concept 을 잘 잡는 것이다.

2. 의미없는 syntax 는 빼고, 최소한의 코드로 code 가 읽기 좋은 것.



-

Kotlin 에서는 clean API 를 위해서 extension function, infix calls, lambda syntax shortcut, operator overloading 등을 제공한다.

@dslmarker, Abstraction, Android UI, anko, API, api clean, api design pattern, api to dsl, apply, apply vs with, assertion, building html with internal dsl, building structured apis, buildstring, call context, chained method call, chaning infix calls, command-query api, compile time, concept, convention, Count, country customer, createHTML, DATABASE, debugging, declarative language, defining extensions on primitive type, dependency, Domain Specific Language, DSL, dsl grammar, dsl naming convention, dsl 장점, dynamic builder, Engine, exposed framework, extension function, extension function tyep, extension function type, external dsl, external template, extract function, file, Framework, function type, function type parameter, Function1, functional interface, functional type, general purpose language, Get, GPL, gradle, Grammer, Groovy, GroupBy, html builder, igher-order function, imperative language, index operator, infix, infix calls, inline, Interface, internal dsl, internal dsl for sql, internal dsls, Invoke, invoke convention, invoke method, Join, kotiln basics, Kotlin, kotlin builders, kotlin dsl, kotlin framework, kotlin tutorial, kotlintest, kotlinx.html, lambda invoke, lambda syntax shortcut, lambda this, lambda with receiver, lambda 의 this, lambda 쪼개기, lambda 호출, lambdas, lambdas with receiver, lambdas with receivers, learning curve, lib, Limit, markup language, member extension, member extension function, Method, multiple method call, Name, name-resolution rule, named object, nesting lambda, object, objects callable as functions, operator overloading, orderby, primitive type extension, qualifier, receiver object, receiver type, receivers, Regular Expression, reuse, Scope, selectall, should, singleton object, slice, SQL, SqlExpressionBuilder, static type, string literal, structure, structure of dsls, syntax, This, this@functionName, this@label, type-safe, Unit, With, [Kotlin Tutorial] DSL construction - Chap 11., 가독성, 유지보수성, 읽기 좋은 것, 접근제한, 최소한의 code, 코틀린, 코틀린 강좌, 코틀린 기초 강좌


-

다른 language 와 마찬가지로 Kotlin 의 DSL 들 역시 fully statically typed 된 형태이다.



-

아래는 DSL 의 예제이다.

val yesterday = 1.days.ago


fun createSimpleTable() = createHTML().

    table{

        tr{

            td{ +”cell” }

        }

    }





11.1.1. The concept of domain-specific languages


-

가장 대표적인 DSL 은 SQL 과 regular expression 이다.

DSL 은 대부분 declarative 하다.

imperative language 는 operation 을 수행하는 step 을 정확히 묘사하는 것이고, declarative language 는 원하는 결과를 얻기 위한 형태의 명령만 하고, operation 수행은 engine 등에 맡긴다.



-

DSL 은 그 specific domain 에서는 두각을 나타내지만 GPL ( General Purpose Language ) 과 혼재되어 사용하기는 어렵다는 단점이 있다.

보통 다른 file 에 쓰거나 string literal 로 작성해서 호출하는 방식을 사용한다.

그래서 compile time 에 이들의 상호작용을 검증하기가 쉽지 않고, debugging 도 쉽지 않다. 

IDE 에서도 잘 지원해주지 않는다.

사용하는 사람은 Syntax 가 달라 배우는 데 시간도 걸리고, 읽는 사람 역시 배움이 필요하다.


DSL 의 장점은 유지하면서 이런 문제를 해결하기 위해서 internal DSLs 컨셉이 최근 인기가 좋다.




11.1.2. Internal DSLs


-

external DSL 은 그 자체의 독자적인 syntax 를 가지고 있는 것을 이야기한다.

internal DSL 은 그 반대 개념으로 GPL 로 쓰여진 것을 이야기한다.



-

Exposed framework 는 database access 를 위한 Kotlin framework 이다.

SELECT Country.name, COUNT(Customer.id)

    FROM Country JOIN Customer

    ON Country.id = Customer.country_id

    GROUP BY Country.name

    ORDER BY COUNT(Customer.id) DESC

    LIMIT 1


(Country join Customer)

    .slice(Country.name, Count(Customer.id))

    .selectAll()

    .groupBy(Country.name)

    .orderBy(Count(Customer.id), isAsc = false)

    .limit(1)


아래의 Exposed framework 로 만든 코드는 위와 동일한 SQL query 문을 만든다.




11.1.3. Structure of DSLs


-

DSL 이 일반적인 API 와 다른 점은 structure 나 grammar 가 있다는 것이다.


일반적인 lib 은 method 들로 구성되어 있고, client 는 lib 의 method 를 하나 하나 호출한다.

이러한 호출에는 상속관계의 structure 나 call 간의 유지되는 context 는 없다.

이러한 일반적인 호출을 command-query API 라고 한다.


반대로 DSL 에는 DSL grammar 에 의해 정의된 structure 가 있다.

Kotlin 에서는 이를 보통 nesting lambda 나 chained method call 을 통해 한다.

저것들이 internal DSL 이 된다.



-

DSL structure 의 한 가지 장점은 여러개의 function call 에 대해 한개의 context 를 물고 가도록 한다.

dependencies{ // lambda nesting structure

    compile(“junit:jnuit:4.11”)

    compile(“com.google.inject:guice:4.1.0”)

}


command-query API 에서는 다음과 같이 call 했어야 한다.

project.dependencies.add(“compile”, “jnuit:jnuit:4.11”)

project.dependencies.add(“compile”, “com.google.inject:guice:4.1.0”)



-

Chained method call 역시 DSL 의 structure 를 구성한다.

str should startWith(“kot”) // infix 와 혼합된 chained method calls @ kotlintest framework


assertTrue(str.startsWith(“kot”))




11.1.4. Building HTML with an internal DSL


-

kotlinx.html lib 를 이용한 코드이다.

fun createSimpleTable() = createHTML().

    table{

        tr{

            td{ + “cell” }

        }

    }


<table>

    <tr>

        <td>cell</td>

    </tr>

</table>



-

왜 text 로 안 하고 Kotlin 으로 코드를 짜려고 하냐고?

Kotlin version 은 type-safe 하다.

예를 들어 td tag 는 tr tag 안에서만 존재할 수 있다. ( 아니면 compile fail )


그리고 이 녀석은 일반적인 코드이기 때문에 다른 langauge 특성들을 다 적용할 수 있다.

fun createAnotherTable() = createHTML().table{

    val numbers = mapOf(1 to “one”, 2 to “two”)

    for ((num, string) in numbers){

        tr{

            td { +“$num” }

            td { + string }

        }

    }

}





11.2. Building structured APIs: Lambdas with receivers in DSLs


-

Lambda with receiver 는 structure 를 만들 수 있게 해주는 Kotlin 의 강력한 feature 이다.




11.2.1. Lambdas with receivers and extension function types


-

fun buildString(builderAction: (StringBuilder) -> Unit) : String{

    val sb = StringBuilder()

    builderAction(sb)

    return sb.toString()

}


val s = buildString{

    it.append(“Hello, “)

    it.append(“World!”)

}

println(s)


여기서의 문제 중 하나는 it 이 반복된다는 것.. 우리는 with 로 이걸 없앨 수 있다는 것을 안다.



-

fun buildString(builderAction: StringBuilder.() -> Unit) : String{

    val sb = StringBuilder()

    sb.builderAction()

    return sb.toString()

}


val s = buildString{

    this.append(“Hello, “)

    append(“World!”)

}

println(s)


buildString 정의를 보면 일반적인 function type 대신 extension function type 을 사용하도록 바뀌었다.

extension function type 은 function type parameter 중 하나를 꺼내서 앞에다 주고 . 으로 구분해주면 된다.


(StringBuilder) -> Unit 이 StringBuilder.() -> Unit 이 되었다.

여기서 StringBuilder 를 receiver type 이라고 부른다. 

그리고 lambda 에 전달되는 value 는 receiver object 가 된다.


왜 extension function type 이냐고?

명시적인 qualifier 없이 external type 의 member 를 접근하는 것이 extension function 과 비슷하기 때문.

그리고 extension function 은 기존 class 에 member 를 추가하는 것임.


결과적으로 extension function 과 lambdas with receiver 모두 receiver object 를 가지며,

function 이 호출할 때 body 에서 접근이 가능하다.


extension function 이기 떄문에 builderAction(sb) 에서 sb.builderAction() 으로 바뀌었다.



-

Lambda 가 receiver 가 있는지 확인해보면 qualifier 없이 호출 할 수 있는지 확인할 수 있다.


StringBuilder.() -> Unit 이라면 StringBuilder 를 qualifier 없이 호출 할 수 있다.



-

실제 lib 에 적용된 buildString function 은 아래와 같다.

fun buildString(builderAction: StringBuilder.() -> Unit): String = StringBuilder().apply(builderAction).toString()



-

apply 는 with 와 동일하지만 lambda 실행 후 receiver object 를 전달한다는 차이가 있다.

inline fun<T> T.apply(block: T.() -> Unit): T{

    block()

    return this

}


inline fun <T,R> with(receiver:T, block: T.() -> R): R = receiver.block()



val map = mutableMapOf(1 to “one”)

map.apply { this[2] = “two” }

with(map) { this[3] = “three” }




11.2.2. Using lambdas with receivers in HTML builders


-

Kotlin 의 HTML 용 DSL 을 HTML builder 라고 부른다.

Idea 는 Groovy 의 dynamic builder 로부터 왔지만, Kotlin 의 것은 사용성이 더 좋고, 안전하며, 추가적으로 더 매력적인 것들이 있다.



-

fun createSimpleTable() = createHTML().

    table{

        tr{

            td { +”cell” }

        }

    }


여기서 table, tr, td 는 모두 higher-order function 이다. ( lambda 를 param 으로 받거나 return 하는 녀석 )

이 녀석들은 특별히 그냥 lambda 가 아니라 lambda with receiver 를 받는다.


lambda 를 이용하므로써 name-resolution rule 을 변경한다. 

예를 들면 tr function 은 table object 안에 들어있다고 보면 그 구조가 이해가 갈 것이다.

open class Tag


class TABLE : Tag { // utility class 라서 code 에 직접 나오지 않는다. 그래서 모두 대문자 정의

    fun tr(init : TR.() -> Unit)

}


class TR : Tag {

    fun td(init: TR.() -> Unit)

}


class TD : Tag



-

receiver with lambda 를 사용하지 않으면 아래와 같이 썼어야 한다.

fun createSimpleTable() = createHTML().

    table{

        (this@table).tr{

            (this@tr).td{

                +”cell"

            }    

        }

    }


cf) lambda 에서의 this 는 가장 가까운 receiver 를 가르킨다.

그러나 this@label 을 통해서 바깥의 receiver 를 지정할 수도 있다.

label 이 없는 경우 function name 으로 접근할 수 있다.


Kotlin 1.1 부터는 @DslMarker annotation 으로 내부에서 바깥쪽의 receiver 접근을 제한할 수도 있다.




11.2.3. Kotlin builders: enabling abstraction and reuse


-

DSL 을 쓰면 extract function 을 해서 reuse 하는 것이 쉽지 않다.

Kotlin + internal DSL 을 사용하면 가능하지롱!



-

<div class=“dropdown”>

    <button class=“btn dropdown-toggle”>

    Dropdown

    <span class=“caret”></span>

    </button>


    <ul class=“dropdown-menu”>

        <li><a href=“#”>Action</a></li>

        <li><a href=“#”>Another action</a></li>

        <li role=“separator” class=“divider”></li>

        <li class=“dropdown-header”>Header</li>

        <li><a href=“#”>Seperated link</a></li>

    </ul>

</div>


fun buildDropdown() = createHTML().

    div(classes=“dropdown”){

        button(classes=“btn dropdown-toggle”){

            +”Dropdown”

            span(classes = “caret”)

        }


        ul(classes = “dropdown-menu”){

            li { a(“#”) { +”Action” } }

            li { a(“#”) { +”Action” } }

            li { role = “separator”; classes=setOf(“divider”) }

            li { classes = setOf(“dropdown-header”); +”Header” }

            li { a(“#”) { +”Seperated link” } }

        }

    }



-

fun dropdownExample() = createHTML().dropdown{

    dropdownButton{ +”Dropdown” }

    dropdownMenu{

        item(“#”, “Action”)

        item(“#”, “Another action”)

        divider()

        dropdownHeader(“Header”)

        item(“#”, “Separated link”)

    }

}


fun UL.item(href: String, name: String) = li { a(href) { +name } }

fun UL.divider() = li{ role = “separator”; classes = setOf(“divider”) }

fun UL.dropdownHeader(text: String) = li {classes = setOf(“dropdown-header”); =text }


fun DIV.dropdownMenu(block: UL.() -> Unit) = ul(“dropdown-menu”, block)







11.3. More flexible block nesting with the "invoke" convention


-

invoke convention 이 있다면 function type 의 object 를 함수로 호출할 수 있다.

이것은 자주 사용되는 패턴은 아니지만 DSL 에서 가끔 유용하게 사용될 수 있다.




11.3.1. The “invoke” convention: objects callable as functions


-

Convention 이라 하면 특별하게 naming 된 함수이며 regular method-call syntax 가 아닌 다른 방식으로 표기하는 것을 지원하는 것을 말한다.

예를 들어 get 을 정의하면 index operator 를 사용할 수 있다.

foo 에 convention 으로 get 을 정의하면 foo[bar] 를 사용할 수 있다.



-

class Greeter(val greeting: String){

    operator fun invoke(name: String){

        println(“$greeting, $name!”)

    }

}


val bavarianGreeter = Greeter(“Servus”)

bavarianGreeter(“Dmitry”)


invoke 는 overload 해서 정의 가능하다. 즉 몇 개의 param 이 들어가도 상관 없다.




11.3.2. The “invoke” convention and functional types


-

lambda 를 호출하는 것은 사실 invoke 를 호출하는 것과 같다.

( lambda() = lambda.invoke() )


lambda 는 inline 되지 않는 한 Function1 과 같은 functional interface 를 구현한 class 가 된다.

그리고 해당 interface 들은 invoke method 를 구현한다.

interface Function2<in P1, in P2, out R>{

    operator fun invoke(p1: P1, p2: P2) : R

}



-

labmda 호출하는 것이 invoke 를 호출하는 것과 같다는 것이 왜 중요한가?

이것이 복잡한 lambda 를 여러개의 method 로 나누는 것을 가능하게 한다.

data class Issue{

    val id: String, val project: String, val type: String,

    val priority: String, val description: String

}


class ImportantIssuePredicate(val project: String) : (Issue) -> Boolean{ // function type base class

    override fun invoke(issue: Issue): Boolean{

        return issue.project == project && issue.imImporant()

    }


    private fun Issue.isImportant(): Boolean{

        return type == “Bug” && (priority == “Major” || priority == “Critical”)

    }

}


val issue1 = Issue(“IDEA-15446”, “IDEA”, “Bug”, “Major”, “Save settings failed”)

val issue2 = Issue(“KT-12183”, “Kotlin”, “Feature”, “Normal”, “Intention: convert several calls”)


val predicate = ImportantIssuePredicate(“IDEA”)

for( issue in listOf(issue1, issue2).filter(predicate)){

    println(issue.id)

}


위의 예에서 single lambda 로 두기에는 로직의 규모가 큰 편이어서, 이 녀석을 function type interface 를 구현하는 class 로 만들었다. 그리고 invoke 를 override 했다.

이 방식으로 코드를 더 깔끔히 쓸 수 있고 로직 분리도 할 수 있다.




11.3.3. The “invoke” convention in DSLs: declaring dependencies in Gradle


-

dependencies.compile(“junit:jnit:4.11”)


dependencies{

    compile(“jnuit:jnuit:4.11”)

}


첫번째 것은 dependencies variable 의 compile 함수 호출이고,

두번째 것은 dependencies 의 invoke 함수가 호출된 후(lambda 를 param 으로 받는다)의 compile 함수 호출이다.





11.4. Kotlin DSLs in practice


11.4.1. Chaining infix calls: “should” in test frameworks


-

s should startWith(“kot”)


infix fun <T> T.should(matcher: Matcher<T>) = matcher.test(this)


interface Matcher<T> {

    fun test(value: T)

}


class startWith(val prefix: String) : Matcher<String>{ // DSL 의 경우 가독성 등을 위해 naming convention 을 깨기도 한다.

    override fun test(value: String){

        if(value.startsWith(prefix) == false)

            throw AssertionError(“String $value does not start with $prefix”)

    }

}



-

“kotlin” should start with “kot”


전혀 코틀린 스럽지 않은 이 코드도 코틀린의 DSL 이다.

“kotlin”.should(start).with(“kot”)


object start // single instance


infix fun String.should(x: start): StartWrapper = StartWrapper(this)


class StartWrapper(val value: String){

    infix fun with(prefix: String) = 

        if (value.startsWith(prefix) == false)

            throw AssertionError(“String does not start with $prefix: $value”)

}


여기서 start 는 single instance 인데 던져주는 게 이상할 수 있다.

문법을 만들기 위해 희생해야 하는 부분이다. 사용자 입장에서는 쉬울 수 있지만, 제작자 입장에서는 소스를 보기 더 힘들 수 있다.




11.4.2. Defining extensions on primitive types: handling dates


-

val yesterday = 1.days.ago

val tomorrow = 1.days.fromNow


val Int.days: Period // Period 는 JDK 8 이긴 함

    get() = Period.ofDays(this)


val Period.ago: LocalDate

    get() = LocalDate.now() - this // Period 에 “minus" 라는 이름의 single param 을 받는 함수가 정의되어 있다.


val Period.fromNow: LocalDate

    get() = LocalDate.now() + this




11.4.3. Member extension function: internal DSL for SQL


-

class Table{

    fun integer(name: String): Column<Int>

    fun varchar(name:String, length:Int): Column<String>


    fun <T> Column<T>.primaryKey(): Column<T>

    fun Column<Int>.autoIncrement(): Column<Int>

    // ...

}


object Country : Table(){

    val id = integer(“id”).autoIncrement().primaryKey()

    val name = varchar(“name”, 50)

}


SchemaUtils.create(Country) // table 만들기


Table class 안의 primaryKey extension 과 autoIncrement extension 해당 class에서만 쓰는 extension 이다.

그래서 이들은 member extension 이라 부른다.

이들은 scope 을 제한한다는 큰 장점을 가진다.



-

member extension 의 단점은 일반 extension 과는 달리 outside 에서 extension 을 추가할 수 없다는 것이다.



-

fun Table.select(where: SqlExpressionBuilder.() -> Op<Boolean>) : Query{ ... }


object SqlExpressionBuilder{

    infix fun<T> Column<T>.eq(t: T) : Op<Boolean>

    ...

}


val result = (Country join Customer)

    .select { country.name eq “USA” }

result.forEach{ println( it[Customer.name] ) }




11.4.4. Anko: creating Android UIs dynmaically


-

class AlertDialogBuilder{

    fun positiveButton(text: String, callback: DialogInterface.() -> Unit)

    fun negativeButton(text: String, callback: DialogInterface.() -> Unit)

    ...

}


fun Context.alert( message:String, title:String, init:AlertDialogBuilder.() -> Unit )



fun Activity.showAreYouSureAlert(process:() -> Unit){

    alert(title = “Are you sure?”, message = “Are you really sure?”){

        positiveButton(“Yes”) { process() }

        negativeButton(“No”) { cancel() }

    }

}



-

verticalLayout{

    val email = editText{

        hint = “Email"

    }


    val password = editText{

        hint = “Password”

        transformationMethod = PasswordTransformationMethod.getInstance()

    }


    button(“Log In”){

        onClick{

            login(email.text, password.text)

        }

    }

}





11.5. Summary


-

Internal DSLs 는 API design pattern 으로 multiple method call 로 구성된 structure를 구성해 더 표현이 좋은 API 를 만드는 것이다.



-

Labmdas with receivers 는 nesting structure 를 만드는 데 좋다.



-

lambda with a receiver 가 취하는 param type 은 extension function type 이다.

그리고 호출하는 function 은 lambda 를 호출할 때 receiver instance 를 전달한다.



-

Kotlin internal DSLs 를 사용하면 external template 이나 markup language 보다 code 재사용성을 높여주고 abstraction 을 만들 수 있다는 장점이 있다.



-

named object 를 infix 의 param 으로 사용하는 것은 DSL 을 영어처럼 읽도록 하는 장점이 있다.



-

primitive type 에서 extension 을 정의할 수 있다.



-

invoke convention 을 사용함으로써 object 를 function 처럼 사용할 수 있다.



-

kotlinx.html 은 HTML page 를 만드는 internal DSL 을 제공한다.



-

kotlintest lib 은 assrtion 기반 unit test 코드를 더 readable 하게 작성하게 도와주는 internal DSL 이다.



-

Exposed lib 은 database 와 연동되는 internal DSL 이다.



-

Anko lib 은 android 개발을 도와주는 녀석이고, UI layout 생성에 대한 internal DSL 도 제공한다.





반응형

댓글