독서관련/Kotlin in Action

CH11 Domain Specific Language

ColinKang 2021. 3. 2. 13:24
아래의 글은 드미트리 제메로프, 스베트라나 이사코바 저/오현석역,『Kotlin in Action』,에이콘출판사(2017)의 내용을 기반으로 작성하였습니다. 

 

DSL을 이용하겨 코틀린 다운  API 설계

수신 객체 지정 람다 : 코드 블록에서 이름(변수)이 가리키는 대상을 결정

invoke 관례를 이용하여 람다와 프로퍼티 대입을 유연하게

 

 

API가 깔끔하다 > 1. 이름, 개념, 코드의 목적이 잘 드러나야함 2. 코드가 간결해야함

 

코틀린의 간결함

Java 구문 Kotlin 구문 기법
StringUtil.capitalize(s) s.capitalize() 확장 함수
1.to("one") 1 to "one" 중위 호출
set.add(2) set += 2 연산자 오버로딩
map.get("key") map["key"] get메소드에 대한 관계
file.use({ f -> f.read()}) file.use { it.read() } 람다를 괄호 밖으로 빼내는 관례
sp.append("a")
sp.append("b")
with (sb) {
   append("a")
   append("b")
}
수신 객체 지정 람다

general purpose programming language : 명령적 : 각 단계를 순서대로 정확히 기술 : C

domain specific language : 선언적 : 원하는 결과를 기술 : 세부 실행은 엔진 : SQL (데이터베이스 조작), REPL (문자열 조작) : 스스로 제공하는 기능을 제한함으로써 오히려 더 효율적으로 자신의 목표를 달성 : DSL을 범용 언어로 만든 호스트 어플리케이션과 함께 조합하기 어렵다

이러한 문제를 해결하면서 DSL의 다른 이점을 살리는 방법으로 internal DSL 개념 사용, 범용 언어로 작성된 프로그램의 일부, DSL의 핵심 장점을 유지하면서 주 언어를 특별한 방법으로 사용하는 것

 

기존 api 관점 : general purpose : command - query 방식

DSL 메소드 호출 : method call chaining, nested loop, combination

 

ex gradle

nested way: 

dependencies {

    compile("junit:junit:4.11")

    compile("com.google.inject:guice:4.1.0")

}

 

command-query way : 중복이 더 많음

project.dependencies.add("compile", "junit:junit:4.11")

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

 

ex 코틀린 test

str should startWith("kot")

 

assertTrue(str.startWith("kot"))

 

 

수신 객체 지정 람다와 확장 함수 타입 : DSL에서 구조화된 api 구축

buildString, with, apply 표준 라이브러리

 

fun buildString (
     builderAction: (StringBuiler) -> Unit // 함수 타입인 파라미터 정의
): String {
     val sb = StringBuilder()
     builderAction(sb)
     return sb.toString()
}

>>> val s = buildString {
     it.append("hello, ")
     it.append("world !")
     // it은 StringBuilder 인스턴스 지정
}

it을 빼려면

람다를 수신 객체 지정 람다 (lambda with a receiver ) 로 변경
람다의 인자 중 하나에게 수신 객체라는 상태를 부여하면, 
이름과 마침표를 명시하지 않아도 그 인자를 멤버로 바로 사용할 수 있음

fun buildString (
     builderAction: StringBuiler.() -> Unit // 수신 객체가 있는 함수 타입의 파라미터 선언
): String {
     val sb = StringBuilder()
     sb.builderAction() // StrinbBuilder 인스턴스를 람다의 수시 객체로 넘긴다.
     return sb.toString()
}

>>> val s = buildString {
     this.append("hello, ") //this 키워드는 StringBuilder 자신
     append("world !") // this가 생략되도 묵시적으로 StringBuilder 인스턴스가 수신 객체로 취급
     // it은 StringBuilder 인스턴스 지정
}

 

파라미터 타입 선언시 일반 함수 타입 선언 대신 확장 함수 타입 extension function type을 사용

확장함수 타입 선언은 람다의 파라미터 목록에 있던 수신 객체 타입을 마라미터 목록을 여는 괄호 앞으로 빼면서 중간에 마침표(.)를 붙인 형태

(StringBuilder) -> Unit을

StringBuilder.() -> Unit으로 변경

앞에오는 (StringBuilder)를 수신객체 타입, 람다에 전다되는 그런 타입의 객체를 수신 객체

 

string.(Int, Int) -> Unit

수신객체타입 (파마리터 타임) 반환타입

 

val appendWeather : StringBuilder.() -> Unit = { this.append("How is weather?")}

>> val stringBuilder = StringBuilder("Hi! ")
>>> stringBuilder.appendWeather()
>>> println(stringBuilder)

Hi! How is Weather!

>>> println(buildString(appendWeather))
How is weather?


 

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


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

inline fun <T> T.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" }
>>> print(map)

{1 = "one", 2 = "two", 3 = "three"}
fun createSimpleTable() = createHtml(). 
	table {
		tr {
    		td { + "cell"
            }
        }    
    }
}
  
  
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

fun createTable() = 
	table {
    	tr {
        	td {
            }
        }
    }
    
>>> println(createTable())
<table><tr><td></td></tr></table>


fdsa

코틀린 빌더 : 추상화와 재사용 : TagConsumer

 

fun StringBuilder.dropdown(
	block: DIV.() -> Unit
) : String = div("dropdown", block)



 

 

invoke 관례

get, set

foo[bar]  foo.get(bar)로 변환

invoke는 () 괄호 사용

 

class Greeter(val greeting: String) {
	operator fun invoke(name: String) {
    	println("$greeting, $name!")
    }
}



>>> val usGreeter = Greeter("Hi")
>>> usGreeter("Colin")
Hi, Colin!


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 {
	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-15111", "IDEA", "Bug", "Major", "Save settings failed")
val i2 = Issue("IDEA-12444", "Kotlin", "Feature", "Normal", "converter")


val predicate = ImportantIssuePredicate("IDEA")

for ( issue in listOf(i1, i2).filter(predicate) {
	print(issue.id)
}

 

a shoudl startWith("kot")


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

"kotlin" should start with "kot"

"kotlin".shuld(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.startWith(prefix))
        	throw AssertionError("String does not start with $prefix: $value")
        else 
        	Unit
}


StartWrapper, EndWrapper, HaveWrapper


import java.time.Period
import java.time.LocalDate

val Int.days: Period
	get() = Period.ofDays(this)
    
val Period.ago: LocalDate
	get() = LocalDate.now() - this
    
val Period.fromNow: LocalDate
	get() = LocalDate.now() + this
    
>>> println(1.days.ago)
2016-08-16
>>> println(1.days.fromNow)
2016-08-18