코틀린 인 액션 11장

존스노우·2023년 4월 30일
0

코틀린

목록 보기
10/10

DSL 영역 특화 언어

  • Domain Specific Language
  • 선언적 선언!

깔끔한 API 작성할 수 있게 돕는 코틀린 기능

  • 확장 함수 / 중위 호출 / 연산자 오버로딩 / 관례 / 수신객체지정 람다.

중위 연산자 - 호출할때 읽기 쉽고 표현력이 좀 더 좋게.

// Define a class named "Person"
class Person(val name: String) {
    // Define an infix member function "isFriendWith"
    infix fun isFriendWith(anotherPerson: Person): Boolean {
        // Just a simple example: two persons are friends if their names have the same length
        return this.name.length == anotherPerson.name.length
    }
}

// Define an infix extension function "hasSameLengthAs" for the String class
infix fun String.hasSameLengthAs(anotherString: String): Boolean {
    return this.length == anotherString.length
}

fun main() {
    val alice = Person("Alice")
    val bob = Person("Bob")
    val carol = Person("Carol")

    // Use the infix function "isFriendWith"
    val aliceAndBobAreFriends = alice isFriendWith bob
    val aliceAndCarolAreFriends = alice isFriendWith carol

    println("Alice and Bob are friends: $aliceAndBobAreFriends") // true
    println("Alice and Carol are friends: $aliceAndCarolAreFriends") // false

    // Use the infix extension function "hasSameLengthAs"
    val stringsHaveSameLength = "Kotlin" hasSameLengthAs "Python"
    println("Kotlin and Python have the same length: $stringsHaveSameLength") // true
}

  • 함수의 인자는 1개 여야 되고.
  • infix 키워드가 있어야 된다.

관례

  • 코틀린 큐칙을 읽고 유지하며 이해하기 쉬운 코드를 만들기위한 권장 사례 지침?
  • 함수 속성 , 지역변수 카멜 케이스 / 클래스 및 인터페이스 이름은 파스칼 케이스..
  • 상수는 대문자로만
  • if else -> when 지향
  • 확장 기능 및 속성을 사용해 기존 클래스 기능 추가.

영역 특화 언어 개념

  • DSL 은 선언적 개념임 , 원하는 결과를 기술 하고 세부 실행은 엔진에 맡김 .
  • 원하는 결과를 얻기 위해 중점을 둠 .
  • Ex) SQL문..

내부 DSL

  • 독립적인 외부 DSL과는 다르게 범용언어로 작성된 프로그램 일부인 내부 DSL

    // 이런식으로 sql문을 내부 DSL 로 작성 가능  결과가 네이티브 코틀린 객체 .
    (Country join Customer)
       .slice(Country.name, Count(Customer.id))
       .selectAll()
       .groupBy(Country.name)
       .orderBy(Count(Customer.id), isAsc = false)
       .limit(1)
    	```
    
    
    ## DSL 구조
    
    
  • 람다 중첩이나 메소드 연쇄시키는 방식으로 구조를 만듬 .

  • 여러 함수를 호출

  • gradle 의존관계 정의시 이것도 DSL! 재사용성 을 보여줌

내부 DSL로 HTML 만들기

  • 코틀린으로 HTML 만들면 ?

  • 타입 안정성 보장 , Td는 tr안에서만 작성 해야 되!

    fun createAnotherTable() = createHTML().table {
       val numbers = mapOf(1 to "one", 2 to "two")
       for ((num, string) in numbers) {
           tr {
               td { +"$num" }
               td { +string }
           }
       }
    }
    

    구조화 된 API : DSL에서 수신 객체 람다.

  • 매번 본문에 It를 이용해 인스턴스를 참조해야되서 번거로움..

  • 수신 객체 지정람다로 정해 구문이 좀더 간결해 졌다.
  • sb.builderAction 으로 수신 객체 넘김
  • 파라미터 선언시 확장 함수 타입을 사용함.
  • 여기서 StringBuilder를 수신객체 타입이라 한다.

  • 책에서 이해 하기쉽게 수신객체람다 동작 설명해 첨부한다.

수신 객체 지정 람다를 HTML 빌더 안에 사용

   open class Tag

   class TABLE : Tag() {
       fun tr(init: TR.() -> Unit) {
           // Implementation
       }
   }

   class TR : Tag() {
       fun td(init: TD.() -> Unit) {
           // Implementation
       }
   }

   class TD : Tag()

  • 수신객체 람다를 이용해 규칙과 구조를 설정하면 이런식으로 코드가 나온다.
  • 이해하기 쉬워지고 간결해 짐.
open class Tag(val name: String) {
    private val children = mutableListOf<Tag>()

    protected fun <T : Tag> doInit(child: T, init: T.() -> Unit) {
        child.init()
        children.add(child)
    }

    override fun toString() = "<$name>${children.joinToString("")}</$name>"
}

fun table(init: TABLE.() -> Unit) = TABLE().apply(init)

class TABLE : Tag("table") {
    fun tr(init: TR.() -> Unit) = doInit(TR(), init)
}

class TR : Tag("tr") {
    fun td(init: TD.() -> Unit) = doInit(TD(), init)
}

class TD : Tag("td")

fun createTable() = table {
    tr {
        td { }
    }
}

fun main() {
    println(createTable())
}
  • HTML을 DSL 방식으로 사용해 만들어내는 코드
  • 모든 태그에는 중첩 태그 저장 리스트가 있고 / 각 태그는 자기 이름을 태그 안에 넣고
  • 자식 태그를 재귀적 문자열로 바꾸 ㅓ합침 -> 닫는 태그 추가 방식.

코틀린 빌더 : 추상화 재사용 가능하게 하는 도구

fun dropdownExample() = createHTML().dropdown {
   dropdownButton { +"Dropdown" }
   dropdownMenu(
       item("#", "Action"),
       item("#", "Another action"),
       divider(),
       dropdownHeader("Header"),
       item("#", "Separated link")
   )
}
  • 기존 HTML 에서 불필요한 세부사항을 제거하고 함수로 감춰둠.

    • fun UL.item(href: String, name: String) = li {
      a(href) { +name }
      }
    • 이런 식으로.
    • fun DIV.dropdownMenu(block: UL.()- >Unit)= ul("dropdown-menu", block)
    • 드롭다운 메뉴로 이런식.
    • 결론은 추상화를 사용해 DSL를 활용한 코드 재사용을 보여주고 있음.

    Invoke 관례를 사용한 더 유연한 블록 중첩

  • 객체를 함수처럼 호출해보자

    class Greeter(val greeting: String) {
     operator fun invoke(name: String) {
         println("$greeting, $name!")
     }
    }
    
    val bavarianGreeter = Greeter("Servus")
    bavarianGreeter("Dmitry") // Output: Servus, Dmitry!
    
  • 함수 앞에 operator 변경자를 붙은 Invoke 메서드를 객체를 함수처럼 호출가능

  • Invoke 관례와 함수 타입

    • 일반적인 람다 호출 방식이 (람다 뒤에 괄호 붙이는 방식) invoke 관례에 지나지 않았음.
    • 인라인 제외 모든 람다는 함수형 인터페이스를 구현하는 클래스로 컴파일 됨.
    • interface Function2(in p1, inp2, out R> {
      operator fun invoke(p1: P1, p2:P2) : R
    • 따라서 함수인 것처럼 람다를 호출하면 Invoke 메서드 호출로 변환 됨.
    // Define a simple lambda
    val lambda = { x: Int, y: Int -> x + y }
    
    // Call the lambda as if it were a function
    val sum = lambda(3, 4)
    println("Sum of 3 and 4 is: $sum") // Output: Sum of 3 and 4 is: 7
    
    // Define a custom class with an 'invoke' operator function
    class Multiplier(val factor: Int) {
        operator fun invoke(x: Int): Int {
            return x * factor
        }
    }
    
    // Create an instance of the custom class
    val triple = Multiplier(3)
    
    // Call the instance as if it were a function
    val result = triple(5)
    println("Triple of 5 is: $result") // Output: Triple of 5 is: 15
    
    • 예제 코드 아직이해가 안됀다.
        data class Issue(
            val id: String,
            val project: String,
            val type: String,
            val priority: String,
            val description: String
        )

        class ImportantIssuesPredicate(val project: String) : (Issue) -> Boolean {
            override fun invoke(issue: Issue): Boolean {
                return issue.project == project && issue.isImportant()
            }

            private fun Issue.isImportant(): Boolean {
                return type == "Bug" && (priority == "Major" || priority == "Critical")
            }
        }

        val i1 = Issue(
            "IDEA-154446",
            "IDEA",
            "Bug",
            "Major",
            "Save settings failed"
        )

        val i2 = Issue(
            "KT-12183",
            "Kotlin",
            "Feature",
            "Normal",
            "Intention: convert several calls on the same receiver to with/apply"
        )

        val predicate = ImportantIssuesPredicate("IDEA")

        for (issue in listOf(i1, i2).filter(predicate)) {
            println(issue.id)
        }
  • IDEA-154446 결과 값.
  • 코드가 복잡한대. invoke 예시를 잘보여주는 듯하다.
  • 객체를 생성 -> Invoke 관례
  • 람다를 여러 메서드로 나누고. 명확한 이름을 붙여줘 코드를 읽히기 쉽게.
  ```
  data class Person(val name: String, val age: Int)

  class AgeFilter(val minAge: Int, val maxAge: Int) : (Person) -> Boolean {
      override fun invoke(person: Person): Boolean {
          return person.isWithinAgeRange()
      }

      private fun Person.isWithinAgeRange(): Boolean {
          return age in minAge..maxAge
      }
  }

  fun main() {
      val people = listOf(
          Person("Alice", 30),
          Person("Bob", 25),
          Person("Charlie", 40),
          Person("David", 20)
      )

      val ageFilter = AgeFilter(25, 35)

      val filteredPeople = people.filter(ageFilter)

      println(filteredPeople)
  }
  • 같은 예시인데 다른 버전 .. 뭔가 감히 잡히는거 같다.

DSL의invoke 관례 :그레이들에서의존관계정의

  • dependencies.compile("junit:junit:4.11")

dependencies {
compile("junit:junit:4.11")
}

  • 두개의 형식을 모두 허용하는 gradle 의존관계 설정을 만들 수는 없을 까?
class DependencyHandler {
    fun compile(coordinate: String) {
        println("Added dependency on $coordinate")
    }

    operator fun invoke(body: DependencyHandler.() -> Unit) {
        body()
    }
}

val dependencies = DependencyHandler()
dependencies.compile("org.jetbrains.kotlin:kotlin-stdlib:1.0.0") // Output: Added dependency on org.jetbrains.kotlin:kotlin-stdlib:1.0.0

dependencies {
    compile("org.jetbrains.kotlin:kotlin-reflect:1.0.0")
} // Output: Added dependency on org.jetbrains.kotlin:kotlin-reflect:1.0.0
  • complie함수도 사용가능하고 , 여러개의 람다 인자를 넘겨주는것을 가능하게하는 구현

  • 두번째 코드는 결과?

  • dependencies.invoke {
    this.compile("org.jetbrains.kotlin:kotlin-reflect:1.0.0")
    }

실전 코틀린 DSL

중위호출 예시

  • s should startwith(“kot”)

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

    interface Matcher<T> {
        fun test(value: T)
    }
    
    class StartWith(val prefix: String) : Matcher<String> {
        override fun test(value: String) {
            if (!value.startsWith(prefix)) {
                throw AssertionError("String $value does not start with $prefix")
            }
        }
    }
    
  • 이런식으로 구현가능한게 신기하다. 좀더 익숙해지게 공부를 하면 뭔가 더 좋은 코드를 짤 수 잇을거같다

  • 아직까지 100퍼 완전 내것이 된건아니지만 코드를 보니 저렇게도 짤수잇구나 신기하다

    // "kotlin”. should(start).with("kot")
    object Start
    
    infix fun String.should(x: Start): StartWrapper = StartWrapper(this)
    
    class StartWrapper(val value: String) {
      infix fun with(prefix: String) {
          if (!value.startsWith(prefix)) {
              throw AssertionError("String does not start with $prefix: $value")
          } else {
              Unit
          }
      }
    }
  • 이런식으로 중위연산자를 활용하면 테스트 코드를 좀더 명확히? 짤 수 있게된다.

  • 한번 이런식으로 짜두는 것도 좋을듯.. 연습하자

profile
어제의 나보다 한걸음 더

0개의 댓글