반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
Tags
- 롬복 어노테이션
- 다리를 지나는 트럭
- 모던 자바 인 액션
- 알고리즘
- @configuration
- @EnableScheduling
- 기능개발
- H-index
- 커링
- 프로그래머스
- 코딩 테스트
- 해시
- 루씬 인 액션
- Java
- @Data
- 스택/큐
- 완주하지 못한 선수
- 전화번호 목록
- 고차원 함수
- kubenetes
- @Setter
- 영속 자료구조
- 정렬
- 가장 큰 수
- @Getter
- K번째수
- 스프링 스케쥴러
- 크론 표현식
- 쿠버네티스
- 검색 기능 확장
Archives
- Today
- Total
Today I Learned
내부 DSL 만들기 본문
728x90
- 범용 프로그래밍 언어가 가진 엄격하고 완전한 능력이 필요할 때가 있다.
- 반면에 특화되고, 규모가 작고 효율적인 도메인 특화 언어(domain-specific languages :DSL)이 필요한 경우도 있다.
- CSS, 정규표현식, XML, Gradle 또는 Rake 빌드파일, React JSX 등도 모두 DSL이라 부를 수 있다.
- 이번 챕터는 DSL을 사용하는 방법이 아니라 DSL을 설계하는 방법을 살펴본다.
DSL의 타입과 특징
외부 DSL vs 내부 DSL
- 외부 DSL은 자유도가 높지만 DSL을 파싱하고 처리할 파서를 만들어야 한다.
- 내부 또는 임베디드 DSL은 언어의 컴파일러와 툴들이 파서의 역할을 해주지만, 자연스럼과 표현력을 확보하기 위한 노력이 필요하다.
컨텍스트 주도와 유창성
- DSL은 컨텍스트 주도적이고 유창성이 높다.
- 컨텍스트는 커뮤니케이션을 간결하고 명확하게 만들어주며 표현력을 강화해준다. 또한 에러의 가능성을 줄여준다.
- 유창성은 노이즈를 줄여주는 동시에 아이디어를 표현하기 쉽게 만들어준다.
내부 DSL을 위한 코틀린
- 일반적으로 내부 DSL을 호스트로 쓰려는 언어에서 정접 타입은 큰 한계가 되지만, 코틀린의 특별한 기능들이 내부 DSL을 만들 때 도움이 되어준다.
생략 가능한 세미콜론
- 흐름을 방해하고 많은 상황에서 노이즈로 작용하는 세미콜론을 생략할 수 있다.
infix를 이용한 점과 괄호제거
- infix 키워드를 이용한 중위표기법 사용으로 점과 괄호를 제거할 수 있다.
확장 함수를 이용한 도메인 특화
- 함수를 인젝팅하면 다음과 같은 코드를 쓸 수 있다.
2.days(ago)
- 게다가 확장 함수에는 infix 키워드를 사용할 수 있다.
2 days ago
- 위와 같은 자연스러운 코드와 표현력을 얻을 수 있다.
람다를 전달할 때 괄호는 필요없다
- 함수의 마지막 파라미터 타입이 람다 표현식이라면 람다를 괄호 밖에 위치시킬 수 있다.
- 또한 함수가 람다 하나만을 아규먼트로 받는다면 호출할 때 괄호가 필요 없다.
- 함수가 클래스에 연관되어 있다면 infix 키워드에 의해 점과 괄호도 생략할 수 있다.
"Release Planning".meeting({
starts.at(14.30);
ends.by(15.20);
})
- 위와 같은 코드를 다음과 같이 바꿀 수 있다.
"Release Planning".meeting {
starts at 14.30
ends by 15.20
}
DSL 생성을 도와주는 암시적 리시버
placeOrder {
an item "Pencil"
an item "Eraser"
complete {
this whit creditcard number "1234-5678-1234-5678"
}
}
- 위 코드에는 주문 컨텍스트와 결제 컨텍스트가 있고, 결제 트랜잭션을 실행하기 위해서는 두 컨텍스트가 모두 필요하다.
- 암시적 리시버를 사용하면 파라미터를 전달하거나 전역상태를 사용할 필요 없이 코드의 레이어 간에 프로세스를 진행하기 위해 상태를 전달하는 것을 쉽게 만들어준다.
DSL을 돕기 위한 추가 특징
- Any 클래스의 메소드들이 람다를 실행시켜주고 암시적 리시버를 세팅해줘서 코드를 줄여준다.
- 현재 객체를 참조하기 위한 this와 람다 표현식의 단일 파라미터를 참조하기 위한 it 키워드를 통해 문법을 표현력있게 만들 수 있다.
유창성 확립 시 마주하는 난관
확장함수 사용
- 이벤트와 날짜를 추적하는 어플리케이션을 만들고 있다고 가정해보자.
package datedsl
import java.util.Calendar
import datedsl.DateUtil.Tense.*
infix fun Int.days(timing: DateUtil.Tense) = DateUtil(this, timing)
- days() 메소드는 DateUtil.Tense enum 값을 받아 DateUtil 클래스의 인스턴스를 리턴한다.
class DateUtil(val number: Int, val tense: Tense) {
enum class Tense {
ago, from_now
}
override fun toString(): String {
val today = Calendar.getInstance()
when (tense) {
ago -> today.add(Calendar.DAY_OF_MONTH, -number)
from_now -> today.add(Calendar.DAY_OF_MONTH, number)
}
return today.getTime().toString()
}
}
- DateUtil 클래스는 Tense enum를 담고 있고 생성자 파라미터를 이뮤터블 속성으로 저장하는 클래스다.
- toString() 메소드는 tense 변수에 따라 다른 처리를 하여 그에 맞는 시간 인스턴스를 리턴한다.
import datedsl.*
import datedsl.DateUtil.Tense.*
println(2 days ago)
println(3 days from_now)
리시버와 infix 사용
"Release Planning" meeting {
start at 14.30
end by 15.20
}
- 위 코드를 동작하게 하기 위해서는 먼저 meeting() 메소드를 String 클래스에 확장 함수로 인젝트해야 한다.
- 그리고 meeting()을 infix 메소드로 만들어서 점을 제거해야 한다.
infix fun String.meeting(block: () -> Unit) {
println("step 1 accomplished")
}
"Release Planning" meeting {}
- 다음으로 상태 업데이트를 위해 회의의 세부 상항인 상태를 가지고 있는 Meeting 클래스를 사용해서, 람다 내부에서 상태를 업데이트해보자.
class Meeting
infix fun String.meeting(block: Meeting.() -> Unit) {
val meeting = Meeting()
meeting.block()
println(meeting)
}
"Release Planning" meeting {
println("With in lambda: $this")
}
- String.meeting() 메소드의 block 파라미터는 Meeting 타입의 리시버를 받는다.
- String.meeting() 메소드 안에서 Meeting 인스턴스를 만들고, 이 인스턴스는 컨텍스트에서 람다를 실행한다.
- 다음으로 Meeting 클래스에 at과 by 메소드를 만들고 람다 안에서 둘 다 실행시켜보자.
class Meeting(val title: String) {
var startTime: String = ""
var endTime: String = ""
private fun convertToString(time: Double) = String.format("%.02f", time)
fun at(time: Double) { startTime = convertToString(time) }
fun by(time: Double) { endTime = convertToString(time) }
override fun toString() = "$title Meeting starts $startTime ends $endTime"
}
infix fun String.meeting(block: Meeting.() -> Unit) {
val meeting = Meeting(this)
meeting.block()
println(meeting)
}
"Release Planning" meeting {
at(14.30)
by(15.20)
}
- at() 메소드가 주어진 Double 값을 String으로 컨버팅한 이후에 startTime 속성에 저장하고, by 메소드는 endTime을 저장한다.
- toString() 메소드는 Meeting 객체의 상태를 보고한다.
- 기대한대로 잘 동작하지만, DSL이 의도에서 너무 벗어나 버렸고 at과 by가 무엇을 의미하는지 알기 어렵다.
infix fun at(time: Double) { startTime = convertToString(time) }
infix fun by(time: Double) { endTime = convertToString(time) }
"Release Planning" meeting {
this at 14.30
this by 15.20
}
- 괄호를 제거하기위해 at()과 by()에 infix를 사용했다. 하지만 infix를 사용하려면 메소드가 호출될 인스턴스가 필요하다.
- at 앞에 인스턴스의 참조를 위해 잠시 this를 사용해야 한다.
class Meeting(val title: String) {
var startTime: String = ""
var endTime: String = ""
val start = this
val end = this
...
"Release Planning" meeting {
start at 14.30
end by 15.20
}
- start와 end 변수에 this를 바인딩해서 this 대신 사용할 수 있도록 했다.
- 하지만 DSL을 사용하는 유저가 start at 대신 start by를 호출하거나 end at을 호출하는 것을 막을 수 없다.
- at과 by 메소드를 분리된 클래스로 옮겨서 잠재적인 에러를 예방할 수 있다.
open class MeetingTime(var time: String = "") {
protected fun convertToString(time: Double) = String.format("%.02f", time)
}
class StartTime : MeetingTime() {
infix fun at(theTime: Double) { time = convertToString(theTime) }
}
class EndTime : MeetingTime() {
infix fun by(theTime: Double) { time = convertToString(theTime) }
}
class Meeting(val title: String) {
val start = StartTime()
val end = EndTime()
override fun toString() =
"$title Meeting starts ${start.time} ends ${end.time}"
}
infix fun String.meeting(block: Meeting.() -> Unit) {
val meeting = Meeting(this)
meeting.block()
println(meeting)
}
"Release Planning" meeting {
start at 14.30
end by 15.20
}
- MeetingTime은 베이스 클래스로서 동작하고 String 타입의 time 속성을 가지고 있다.
- startTime 클래스는 MeetingTime을 확장한 클래스이고 at() 메소드를 가지고 있다.
- End 클래스는 at() 대신 by() 메소드를 가지고 있다.
- Meeting의 start 속성은 startTime의 인스턴스를 리턴하고, end 속성은 endTime의 인스턴스를 리턴한다.
728x90
'Kotlin > 다재다능 코틀린 프로그래밍' 카테고리의 다른 글
코루틴 탐험하기 (0) | 2023.11.02 |
---|---|
코틀린에서 구현하는 유창성 (2) (1) | 2023.10.19 |
코틀린에서 구현하는 유창성 (1) (0) | 2023.10.13 |
내부 반복과 지연 연산 (0) | 2023.09.22 |
람다를 사용한 함수형 프로그래밍(2) (0) | 2023.09.22 |
Comments