라이브러리를 설계하는 것은 실제로 언어를 설계하는 것입니다(Bell Labs 격언)
C# 언어에서 링크 You can run Map/Reduce/Filter라는 문법이 SQL 쿼리문과 유사한 구문으로 ORM(Entity Framework)에서 지원하므로 실제 쿼리를 실행하는 것처럼 C# 코드로 작성할 수 있습니다.
물론 언어에 내장된 DSL이기 때문에 컴파일을 확인하고 IDE에서 도움을 받고 원하는 대로 조작할 수 있습니다. (기능의 조합이 가능합니다.)
C#에서 Kotlin(JAVA)으로 전환한 후 가장 불편한 점 중 하나입니다.
단점은 이 유형의 SQL에 대한 DSL이 없다는 것입니다.
Spring Boot의 JPA 방법인 함수 이름으로 SQL을 생성하는 기이한 방법은 말할 것도 없습니다. 이 문제를 해결하기 위해 JAVA 시절부터 널리 사용되는 라이브러리인 QueryDsl 또는 JOOQ가 있습니다.
- 주석 기반 메타프로그래밍의 한계. 사전에 컴파일해야 합니다. 언어에 내장된 자연스러운 연결은 없습니다.
- Java 언어의 범위 내에서 빌드된 DSL이므로 JAVA 스타일 DSL(Java 스타일 Fluent API 및 빌더 패턴 등)의 형식을 취합니다.
그 이유는 Kotlin이 JAVA와 너무 잘 통합되어 있기 때문에(이 또한 목표임) 예전에는 JAVA 라이브러리만 사용하는 경향이 강했기 때문입니다.
조용한 라인 코틀린-jdsl 또는 JetBrains/노출 Kotlin을 대상으로 하는 DSL도 있지만 QueryDSL은 SQL DSL을 구축할 때 가장 널리 사용되는 방법인 것 같습니다. 둘 다 국내 최고의 회사인 Line 및 Kotlin을 만드는 JetBrains의 훌륭한 DSL이지만 QueryDSL이 가장 널리 사용되는 방법입니다. (저도 회사에서 JPA와 QueryDSL을 사용합니다)
어쨌든 C#의 LINQ, Clojure 허니SQL 스칼라에서 깃털 언어의 특성을 최대한 활용하는 DSL을 찾던 중 위의 (Kotlin JDSL: Kotlin을 사용하여 더 쉽게 JPA Criteria API 작성(linecorp.com)) 오전.
링크에서 알 수 있듯이 주석 기반 메타프로그래밍이 아니기 때문에 사용하기가 훨씬 쉽습니다. 많은 엔드포인트가 지원되는 것 같습니다.
그냥, 그냥, 하지만… 한 가지 아쉬운 부분은 DSL 형태로 되어 있고 Fluent API 형태로 빌드되었다는 점입니다. 사용성 측면에서 QueryDsl과 유사합니다.
Fluent API는 나쁘지 않습니다. 아마도 세계에서 가장 많이 사용되는 DSL 유형일 것입니다… Fluent API가 DSL인 것은 이해하지만 마음속으로는 인정하지 않습니다…
아마도
- 기존 DSL 양식과 가장 유사한 양식 채택으로 접근성 증대
- 리플렉션과 같은 방법을 최대한 피하여 런타임 성능 극대화
- 일관된 DSL 형식 제공
등등 이런 이유가 있는지 궁금합니다.
다른 Kotlin 기능을 사용하여 더 멋진 DSL을 만들 수 있는 방법이 있는지 궁금해서 이것저것 시도해 보았습니다.
이원준/klos: SQL의 Kotlin 레이어(github.com)
GitHub – 이원준/klos: Kotlin layer of SQL
SQL의 코틀린 레이어 GitHub에서 계정을 생성하여 Lee-WonJun/klos의 발전에 기여하십시오.
github.com
장난처럼 써보았습니다.
단순한 DSL 형태로 표현되는 것에 집중하고 있었기 때문에 지금은 쓸데없는 레포지만,
Kotlin 스타일의 DSL이 어디까지 갈 수 있는지 내 손으로 확인하는 저장소입니다.
확장 가능한 DSL을 만드는 일반적인 방법은 다음과 같습니다.
- 자신의 언어(예: DSL)에 대한 ADT 설계,
- ADT를 빌드할 수 있는 헬퍼 함수를 모아 DSL 빌더를 배포한 후,
- 해당 ADT는 이제 데이터 집합일 뿐이므로 이를 해석하고 실제로 작동시키는 인터프리터가 생성됩니다.
오전
즉, Bell Labs의 격언(라이브러리 설계는 실제로 언어를 설계하는 것임)과 같이 실제 언어를 만드는 방법을 거의 동일하게 사용할 수 있습니다.
나는 전에 (LolChatLang) 언어를 개발했습니다. 비슷한 방법으로 ADT를 구성할 수 있도록 롤챙 프로그래밍 언어(tistory.com)를 만들어 봅시다.
다만 DSL 프린팅을 주제로 한 포스팅이라 이번에는 인터프리터는 생성하지 않고 DSL 빌더를 구성했습니다.
ADT는 다음과 같습니다. (“select different * from table where cond1 and cond2″는 이 시점까지만 구축할 수 있는 ADT로, SQL 문은 요소가 너무 많아 모두 실행할 수 없었다.)
sealed interface Expr
sealed interface Comparable : Expr
sealed class ColumnExpr<T> : Comparable {
data class Column<T>(val col:KProperty1<T,Any>) : ColumnExpr<T>()
data class Columns<T>(val cols: List<KProperty1<T, Any>>) : ColumnExpr<T>()
data class Asterisk<T : Any>(val entity:KClass<T>) : ColumnExpr<T>()
}
data class LiteralExpr(val value: Any) : Comparable
enum class CompareOperator : Expr {
EQ, NEQ, GT, GTE, LT, LTE
}
enum class LogicalOperator: Expr {
AND, OR
}
sealed class BinaryOp : Expr {
data class Compare(val left: Comparable, val op: CompareOperator, val right: Comparable) : BinaryOp()
data class Logical(val left: BinaryOp, val op: LogicalOperator, val right: BinaryOp) : BinaryOp()
}
sealed class WhereExpr : Expr {
data class Where(val expr: BinaryOp) : WhereExpr()
object Empty : WhereExpr()
}
sealed class DistinctExpr : Expr {
object Distinct : DistinctExpr()
object All : DistinctExpr()
}
data class FromExpr<T:Any>(val entity: KClass<T>)
data class SelectExpr<T:Any>(val columns: ColumnExpr<T>, val distinct: DistinctExpr, val from: FromExpr<T>, val where: WhereExpr)
이 ADT를 수동으로 만드는 경우 다음과 같이 저장할 수 있습니다.
SelectExpr<Person>(
columns = ColumnExpr.Asterisk(Person::class),
distinct = DistinctExpr.Distinct,
from = FromExpr(Person::class),
where = WhereExpr.Where(
BinaryOp.Logical(
left = BinaryOp.Logical(
left = BinaryOp.Logical(
left = BinaryOp.Compare(
left = ColumnExpr.Column(Person::name),
op = CompareOperator.EQ,
right = LiteralExpr("John")
),
op = LogicalOperator.AND,
right = BinaryOp.Compare(
left = ColumnExpr.Column(Person::age),
op = CompareOperator.GT,
right = LiteralExpr(10)
)
),
op = LogicalOperator.AND,
right = BinaryOp.Compare(
left = ColumnExpr.Column(Person::age),
op = CompareOperator.LT,
right = LiteralExpr(20)
)
),
op = LogicalOperator.OR,
right = BinaryOp.Compare(
left = ColumnExpr.Column(Person::name),
op = CompareOperator.EQ,
right = LiteralExpr("Jane")
)
)
)
)
보시다시피 ADT는 한 땀 한 땀 이루어지기 때문에 매우 깁니다.
이것은 아래에 표시된 SQL입니다.
SELECT DISTINCT *
FROM Person
WHERE
(name="John" AND age > 10 AND age < 20)
OR name="Jane"
다음에 이러한 종류의 ADT를 만드는 데 도움이 되는 도우미를 작성하면 DSL이 완료됩니다.
DSL 빌더 코드는 현재 매우 길고 일부 강제 구현이 있습니다. 지름길소설 삽입.
DSL Builder에서 Kotlin의 labmda + 수신자 함수, 중위 연산자, 클래스 및 함수가 올바르게 결합 및 구현되었습니다.
위의 코드는 다음과 같이 생성할 수 있습니다.
Query(Person::class) {
Select Distinct (Person::class)
From (Person::class)
Where {
((col(Person::name) `==` lit("John")) And
(col(Person::age) gt lit(10)) And
(col(Person::age) lt lit(20))) Or
(col(Person::name) `==` lit("Jane"))
}
}
실제 SQL 문과 매우 유사하게 작성되었습니다! 청소
거기에서 Distinct를 빼려면 그냥 빼면 됩니다.
Query(Person::class) {
Select (Person::class)
From (Person::class)
Where {
((col(Person::name) `==` lit("John")) And
(col(Person::age) gt lit(10)) And
(col(Person::age) lt lit(20))) Or
(col(Person::name) `==` lit("Jane"))
}
}
그 이유는 구현이 좀 까다롭긴 하지만 QueryBuilder에서는 이렇게 구현되어 있기 때문입니다.
fun Select(entity: KClass<T>) {
//블라 블라
}
val Select: SelectBuilder<T> = SelectBuilder(entity)
class SelectBuilder<T : Any>(private val entity: KClass<T>) {
infix fun Distinct(entity: KClass<T>) {
// 블라블라
}
}
빌더에서 (Person::class)를 선택하면 현재 (Person::class)를 수신하고 있는 함수를 호출합니다.
Select Distinct(Person::class)는 Select 멤버 변수를 사용하여 중위 연산자 Distinct를 호출하고 Distinct는 (Person::class)를 받는 함수가 됩니다.
한참을 고민하다가 다른 방법이 없다고 생각해서 그렇게 했습니다.
대변은 자바와 달리 DSL을 구축하는 데 필요한 기능이 풍부하기 때문에 이 방법을 최대한 활용하면 보다 자연스러운 DSL을 구축할 수 있을 것 같습니다.
하지만 코틀린으로 DSL을 만들면서 느꼈던 한계는 다음과 같다.
- 운영 과부하가 가능하지만 다소 제한적입니다. 그래서 방금 새로운 중위 연산자를 정의했습니다.
- 약간의 한계입니다. 불가능했다, < und > 함수 이름으로 사용(그래서 >= 대신 `gt=`를 사용했습니다.)
- Java 주석과 유사한 제한된 메타프로그래밍. 매크로가 없으며 moand 구문 또는 빌더가 제공되지 않습니다.
SQL은 상당히 큰 DSL이고 내가 원하는 방식이 모나드 구문으로 커버되지 않은 것 같은데, 가장 단순해서 모나드 구문으로 간단한 DSL을 작성해서 미안하다. - HKT 없음(여기에 쓸 필요는 없었지만…)
- ADT의 봉인된 클래스 구현은 좀 장황하고, Java에 비하면 요정이지만 F# 같은 ML 유형의 언어가 가장 좋은 것 같습니다.