본문 바로가기

프로그래밍 언어 노트/JAVA | Kotlin

Kotlin 의 Lambda 문법으로 DSL 을 구축해보자

DSL 을 Kotlin / Scala 에서 구현할때 자주 사용하는 기법중 하나는 Last Parameter 의 람다 를 {} <- 이거 로 표현할수 있다는것이다

별거 아니라고 생각할수도 있는데, Internal DSL 이라는거 자체가 대충 지금 쓰는 언어에 (선언적으로) 조화롭게 어울려서 잘 들어가 있으면 걍 Internal DSL 이다.

Java 에서는 This 를 이용해서 Fluent API Chain 으로 DSL 을 구축하는거처럼.. (여담. Fluent API 가 DSL 이라는거는 머리로는 이해하지만 가슴으로 인정할수없다)

멋들어지게 프로그래밍을 잘하는 사람은 DSL 을 구축하기 위하여 패턴매칭, infix operator, Monad, Macro 같은 메타 프로그래밍 ... 등등 기술을 이용해서 뚝딱뚝딱 짜서 비즈니스로직을 멋들어지게 구축하는데, 

그정도까지 가지 않아도 마지막 람다 파라미터를 {} 이것으로 표현하는것만으로도 꽤 멋들어진 DSL 을 만들 수 있다.

Kotlin 계열에서 멋들어지게 짜여진 DSL 을 구경하고 싶다면 JetBrains/Exposed: Kotlin SQL Framework (github.com) 참고

 

암튼 마지막 Parameter를 {} 로 표현하면 멋들어진 표현식이 가능하다.

필자는 보통 코드 중복이나 Depth 를  (멋있게) 줄이고싶을때 주로 사용하는데 예를들면 다음과 같다.

fun main(args: Array<String>) {
    val inPath = "C:\\Users\\dldnj\\Desktop\\reader.txt"
    val outPath = "C:\\Users\\dldnj\\Desktop\\writer.txt"

    // DSL 안 쓸때
    FileReader(inPath).use { fileReader ->
        FileWriter(outPath).use { fileWriter ->
            fileWriter.write(fileReader.readText())
        }
    }

    // DSL 쓸때
    fileScope(inPath, outPath) { (fileReader, fileWriter) ->
        fileWriter.write(fileReader.readText())
    }
}


// DSL 용 헬퍼 Class / 함수
data class Rewriter(val fileReader: FileReader, val fileWriter: FileWriter)

fun fileScope(inPath: String, outPath: String, block: (Rewriter) -> Unit) {
    FileReader(inPath).use { fileReader ->
        FileWriter(outPath).use { fileWriter ->
            block(Rewriter(fileReader, fileWriter))
        }
    }
}

DSL 쓰는것이 깔끔하다

 

좀더 로직적인 것을 넣어도 좋다

fun main(args: Array<String>) {
    val (inDir, outDir) = ("C:\\Users\\dldnj\\Desktop\\in" to "C:\\Users\\dldnj\\Desktop\\out")

    // 명령형
    for (count in 1..2) {
        FileReader("$inDir\\$count.txt").use { fileReader ->
            FileWriter("$outDir\\$count.txt").use { fileWriter ->
                fileWriter.write(fileReader.readText())
            }
        }
    }

    // 선언적 DSL
    repeatWrite(2, inDir, outDir) { (fileReader, fileWriter) ->
        fileWriter.write(fileReader.readText())
    }
    
}

data class Rewriter(val fileReader: FileReader, val fileWriter: FileWriter)

fun fileScope(inPath: String, outPath: String, block: (Rewriter) -> Unit) {
    FileReader(inPath).use { fileReader ->
        FileWriter(outPath).use { fileWriter ->
            block(Rewriter(fileReader, fileWriter))
        }
    }
}

fun repeatWrite(repeat: Int, inDirPath: String, outDirPath: String, block: (Rewriter) -> Unit) {
    (1..repeat).forEach { count ->
        fileScope("$inDirPath\\$count.txt", "$outDirPath\\$count.txt", block)
    }
}

 

좀더 광기를 첨가하면 이런직도 가능하다. Feat data class

import java.io.FileReader
import java.io.FileWriter

fun main(args: Array<String>) {
    // 아래 선언만으로 save 폴더에 , save1.txt ~ save5.txt 가 생성된다.
    fileWrite {
        count = 5
        file = file {
            dir = "C:\\Users\\dldnj\\Desktop\\save"
            name = "save$$"
            extension = "txt"
        }
        fileContent = content {
            content = "Hello World"
        }
    }
}


// DSL 용 Helper 
data class file (
    var dir: String = "",
    var name: String = "",
    var extension: String = ""
)

fun file(init: file.() -> Unit): file {
    val file = file()
    file.init()
    return file
}

data class content (
    var content: String = ""
)

fun content(init: content.() -> Unit): content {
    val content = content()
    content.init()
    return content
}

fun fileWrite(block: FileWrite.() -> Unit) = FileWrite().apply(block).write()

class FileWrite {
    var count: Int = 0
    var file: file = file()
    var fileContent: content = content()

    fun write() {
        (1..count).forEach {
            FileWriter("${file.dir}\\${file.name.replace("$$",it.toString())}.${file.extension}").use { fileWriter ->
                fileWriter.write(fileContent.content)
            }
        }
    }
}

물론 이렇게 까지는 잘 안하지만 어디까지 표현할수있는지 알려주기 위하여.. (Helper 가 너무 복잡해짐)

역시 DSL 은 어차피 S-exp 로 표현되는 Clojure 가 최고

 

간지난다고 표현했지만 사실 가독성에 큰 영향을 미치는 요인이다.

꼴랑 파라미터 () 여기에 안들어가고 따로 {} 로 표현된다는 것 뿐이지만, Kotlin 에 맞는 스타일로 표현된다것, 즉 그 언어에 내장된 언어처럼 표현되는것이 DSL 이 가지는 강점이다.

언어를 사용하는 입장에서는, 그 언어다움이 굉장히 중요하다. 함수를 파라미터로 넘기는거는 FP 언어 계열에서는 너무 당연해서 딱히 불편하지도 않다, 하지만 Kotlin 과 같은 문법에서 모든 함수가 그렇게 표현되면 상당히 이질적이다.

728x90