ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Kotlin Type-safe builder 개념 알아보기 - Kotlin DSL!!
    개발하면서/타인글보면서 2024. 10. 9. 11:52
    반응형

    회사에서 Kotlin을 주 언어로 개발하고 있는데 프로젝트 개선을 위해 새로운 라이브러리를 사용하다 보니

    Kotlin의 Type-safe builder를 알아두면 좋을 것 같다.

     

    DSL을 이용해서 새로운 무언가를 만든다기보다는

    현재 쓰고 있는 라이브러리를 확장하고 유연하게(?) 사용하고 싶은데 감으로만 하기에는 한계를 느꼈다.

     

    아래 자료를 읽고 정리했다.

    https://kotlinlang.org/docs/type-safe-builders.html

    https://kotlinlang.org/docs/functions.html

    https://d2.naver.com/helloworld/9879422

    https://toss.tech/article/kotlin-dsl-restdocs

     

    Type-safe builder라는 이름의 Internal DSL 생성 기능은

    Kotlin의 여러 기능을 조합했다고 하는데 필요한 Kotlin 함수 개념을 알아본다.

    • Higher order function
    • Lambda expression
    • Trailing Lambda
    • Function types
    • Invoke operator
    • Function literals with Receiver
    • Infix notation
    • Scope control: @DslMarker


    1.  Higher order function

    Higher order function은 함수를 인자로 받는 함수를 말한다.

    kotlin-stdlib에는 다양한 컬렉션(Array, Iterable, Map)의 확장함수로 map 함수를 정의했는데

    (T) 타입을 R 타입으로 바꾸는 transform 함수를 인자로 받는다.

     

     

    함수를 inline으로 정의한 게 궁금해서 찾아봤다.

    고차 함수는 객체이고 내부 함수에서 외부 함수 변수에 접근 가능한 Closure 개념도 포함된다.

    이를 지원하려면 메모리 할당과 함수 호출이 일어나므로 약간의 페널티가 생긴다.

     

    inline 키워드를 붙이면 함수의 인자로 전달하는 함수의 바이트코드가 그 자리에(?) 복사된다.
    별도의 메모리 할당도 필요 없고 함수 호출도 일어나지 않는다.

    그렇다고 모든 함수를 inline으로 하면 늘어나는 바이트코드가 늘어나 성능에 영향을 줄 수 있으므로

    1~3줄의 함수에만 inline 사용을 권장한다.

     

    2. Lambda expression

    함수 정의를 { ... }로 하는 방식을 말한다.

    val items = listOf(1, 2, 3, 4)
    
    items.map(fun(item): Int { return item * 10 })
    
    // Lambda expression
    items.map({ item -> item * 10 })

     

    3. Trailing Lambda

    함수 마지막 인자가 함수라면 Lambda expression을 ) 이후에 적을 수 있게 해 준다.

    뜬금없지만 비슷하게 Default arguments가 마지막에 있으면 생략하는 게 생각났다.

    items.map { item -> item * 10 }

     

    4.  Function types

    Kotlin에서는 함수를 선언하는데 Function types을 사용한다.

    파라미터나 리턴 값이 함수인 경우 사용된다.

     

    예시 1.   (A, B) -> C

        A와 B 타입을 파라미터로 받아 C 타입을 리턴하는 "Function types"

     

    예시 2.   A.(B) -> C.   receiver type(A)를 추가할 수 도 있다.

        파라미터 B 타입인 Receiver object A를 호출해서 C 타입을 리턴하는 "Function types"

     

    예시 3.  typealias ClickHandler = (Button, ClickEvent) -> Unit

        typealias를 이용하여 이름을 붙일 수 있다.

     

     

    5. Invoke operator

    Function types 호출은 invoke(...)로 가능하다. f.invoke(x)와 f(x)는 동일하다.

    Function type with receiver type인 경우 첫 번째 인자가 Receiver object이다.

    receiver type을 호출하는 또 다른 방법은 마치 확장함수 처럼 Receiver object에 붙여준다.

    val stringPlus: (String, String) -> String = String::plus
    val intPlus: Int.(Int) -> Int = Int::plus
    
    println(stringPlus.invoke("<-", "->"))
    println(stringPlus("Hello, ", "world!"))
    
    println(intPlus.invoke(1, 1))
    println(intPlus(1, 2))
    println(2.intPlus(3)) // extension-like call

     

    또한 해당 객체 자체를 호출하는 operator로 invoke를 사용 가능하다.

    만약 invoke operator 없이 Function types with receiver type을 호출(?) 하려면 아래처럼 별도의 멤버함수를 추가해야 한다.

    // definition
    class TestClass {  
        fun test(block: TestClass.() -> Int) {
            block()
        }
        
        fun doSomething1(): Int = 1
    
        fun doSomething2(): Int = 2
    }
    
    val testClass = TestClass()  
    testClass.test {  
        doSomething1()
        doSomething2()
    }

     

    6. Function literals with Receiver

    앞에서 Function types with receiver type을 알아봤다.  A.(B) -> C

    receiver type이 있는 Function types를 Funciton literals with Receiver라고 부른다.

    receiver type(A.(B) -> C)을 전달한 인자의 함수 본문은 receiver object(A)를 의미하므로

    this 없이 A 멤버에 접근할 수 있다.

     

    7. Infix notation

    fun 앞에 infix를 표기하면 함수 호출할 때 문장처럼 호출할 수 있다.

    infix fun Int.shl(x: Int): Int { ... }
    
    // calling the function using the infix notation
    1 shl 2
    
    // is the same as
    1.shl(2)

    3가지 제약사항이 있다.

    • 반드시 멤버함수 또는 확장 함수 여야한다.
    • 반드시 1개의 파라미터가 있어야 한다. 2개 이상이거나 파라미터가 없으면 안 된다.
    • vargs나 default argument가 아니어야 한다.

     

    8.  Scope control: @DslMarker

    receiver type을 제한 없이 받게 되면 아래처럼 head에 head를 전달할 수 있다. 이는 잘못된 호출이다.

    html {
        head {
            head {} // should be forbidden
        }
        // ...
    }

     

    이때 @DslMarker 어노테이션을 이용하면 구문 오류가 난다. 굳!!

     
    // @DslMarker usage
    @DslMarker
    annotation class HtmlTagMarker
    
    @HtmlTagMarker
    class Html(...)
    
    @HtmlTagMarker
    class Head(...)
    
    // usage
    val html = html {  
        head {
            head { } // error
        }
    }
    /**
     * When applied to annotation class X specifies that X defines a DSL language
     *
     * The general rule:
     * - an implicit receiver may *belong to a DSL @X* if marked with a corresponding DSL marker annotation
     * - two implicit receivers of the same DSL are not accessible in the same scope
     * - the closest one wins
     * - other available receivers are resolved as usual, but if the resulting resolved call binds to such a receiver, it's a compilation error
     *
     * Marking rules: an implicit receiver is considered marked with @Ann if
     * - its type is marked, or
     * - its type's classifier is marked
     * - or any of its superclasses/superinterfaces
     */
    @Target(ANNOTATION_CLASS)
    @Retention(BINARY)
    @MustBeDocumented
    @SinceKotlin("1.1")
    public annotation class DslMarker

     

     

    DSL을 이용하는 라이브러리를 좀더 유연하게 사용하고 싶어 알아봤는데

    오히려 Kotlin의 진가를 맛본 시간이었다.

     

    부작용으로 if문 for문도 DSL로 생각하게 됐다...허허허

    반응형

    댓글

Designed by Tistory.