ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)
    Study(종료)/Kotlin 22.09.13 ~ 12.18 2022. 11. 24. 22:45

    11장

    11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)

    11-1-2.  도메인 특화 언어 : 연산자 오버로딩(6~8)

    11-2.  도메인 특화 언어 : 위임 프로퍼티

    11-3. 도메인 특화 언어 : 고차 함수와 DSL


     

    11-1-1. 도메인 특화 언어 : 연산자 오버로딩(1~5)

     

    연산자 오버로딩은 + - * / 등 코틀린 내장 연산자에 대해

    새로운 의미를 부여할 수 있게 해주는 언어 기능이다.

    당장 떠오르는건 같은 + 연산자를 사용하더라도,

    Number 타입의 경우 덧셈 연산을 하지만 문자 타입의 경우 문자를 합치는 연산을 한다.

    이게 가능한 이유가 +가 오버로딩돼 있어 다양한 구현을 제공하기 때문이라고 한다.

    이러한 기능들을 사용하기 위한 연산자 오버로딩에 대해 좀 더 자세히 알아보자.

     

    1) 단항 연산

    오버로딩이 가능한 단항 연산자로는 전위 + - ! 연산자가 있다.

    컴파일러는 이러한 연산자를 함수 호출로 표현하는 방법을 제공한다.

    의미
    +e e.unaryPlus()
    -e e.unaryMinus()
    !e e.not()

    이런 함수를 인자 식의 타입에 대해 멤버 함수나 확장 함수로 정의할 수 있다.

     

    이를 이용해 not() 컨벤션을  보색 관계를 ! 연산자를 표현할 수 있다.

    enum class Color{
        Black, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, WHITE;
    
        operator fun not() = when (this){
            Black -> WHITE
            RED -> CYAN
            GREEN -> MAGENTA
            BLUE -> YELLOW
            YELLOW -> BLUE
            CYAN -> RED
            MAGENTA -> GREEN
            WHITE -> Black
        }
    }
    fun main() {
        val black = Color.Black
        println(!black) // WHITE
        val red = Color.RED
        println(!red) // CYAN
    }

    연산자 함수를 확장 함수로 정의하면 임의의 타입에 대한 연산자를 오버로딩하는것도 가능하다

    operator fun <T> ((T) -> Boolean).not(): (T) -> Boolean = {!this(it)}
    fun isShort(s: String) = s.length <= 4
    fun String.isUpperCase() = all { it.isUpperCase() }
    fun main() {
        val data = listOf("abc", "ABCDE", "ababc", "BdAc", "xyz")
        println(data.count(::isShort)) // 3
        // abc, ababc, xyz
        println(data.count(!::isShort)) // 2
        // ABCDE, BdAc
        println(data.count(String:: isUpperCase)) // 1
        // ABCDE
        println(data.count(!String:: isUpperCase)) //4
        // abc, ababc, BdAc, xyz
    }

    2) 증가와 감소

    증가(++)와 감소(--)연산자도 피연산자 타입에 대한 파라미터가 없는 inc(), dec() 함수로 오버로딩할 수 있다.

    이 함수의 반환 타입은 증가 아후 값과 증가 이전 값이 같은 타입이어야 한다.

    enum class RainbowColor{
        RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET;
        operator fun inc() = values()[(ordinal + 1) % values().size]
        operator fun dec() = values()[(ordinal + values().size - 1) % values().size]
    }
    
    fun main() {
        var color = RainbowColor.INDIGO
        println(color++) // INDIGO
        println(color) // VIOLET
    }

    3) 이항 연산

    이상 연산자 역시 오버로딩할 수 있다. 마찬가지로 정해진 이름의 연산자 함수를 정의하면 된다.

    의미
    a + b a.plus(b)
    a - b a.minus(b)
    a * b a.times(b)
    a / b a.div(b)
    a % b a.rem(b)
    a .. b a.rangeTo(b)
    a in b b.contains(a)
    a !in b !b.contains(a)
    a < b a.compareTo(b) < 0
    a <= b a.compareTo(b) <= 0
    a > b a.compareTo(b) > 0
    a >= b a.compareTo(b) >= 0

    % 연산자의 이름은 mod() 연산자였지만 rem() 연산자로 대체되었고, mod()는 사용 불가능하다.

    개인적으로 비교 연산을 할 때 함수가 compareTo 연산 이후 0과 비교하는게 신기했다.

     

    이항 연산자중 동등성 관련 연산도 있다. ==나 !=를 사용하면 컴파일러는 equals() 함수를 호출한다.

    여기서 equals() 구현이 Any 클래스에 정의된 기반 구현을 상속하므로

    명시적인 operator 변경자를 붙이지 않아도 된다.

    또한 같은 이유로 equals()를 항상 멤버 함수로 정의해야 한다.

     

    코틀린에서 && 나 || 연산자는 오버로딩할 수 없다.

    이들은 Boolean값에 대해서만 지원되는 내장 연산이다.

    참조 동등성 연산자인 ===나 !==도 마찬가지로 오버로딩할 수 없는 내장 연산이다.


    4) 중위 연산

    중위 연산(infix function)에 대해서 앞장에서 공부한 적이 있다.

    중위 연산은 변수 두 개 사이에 정의된 infix 변경자를 이용한 연산을 하는것으로 기억한다.

    val pair1 = 1 to 2 // 중위 호출
    val pair2 = 1.to(2) // 일반적인 호출

    표준 to 함수 구현은 아래와 같다.

    infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

    이를 이용해 논리곱, 논리합을 표현하는 중위 연산을 정의할 수 있다.

    앞에서 작성했던 함수를 가져와서 비교해보자면,

    operator fun <T> ((T) -> Boolean).not(): (T) -> Boolean = {!this(it)}
    fun isShort(s: String) = s.length <= 4
    fun String.isUpperCase() = all { it.isUpperCase() }
    fun main() {
        val data = listOf("abc", "ABCDE", "ababc", "BdAc", "xyz")
        println(data.count(::isShort)) // 3
        // abc, ababc, xyz
        println(data.count(!::isShort)) // 2
        // ABCDE, BdAc
        println(data.count(String:: isUpperCase)) // 1
        // ABCDE
        println(data.count(!String:: isUpperCase)) //4
        // abc, ababc, BdAc, xyz
    }

    위 함수를 중위 연산 정의를 통해 좀 더 간결하게 만들 수 있다.

    infix fun <T> ((T) -> Boolean).and(
            other: (T) -> Boolean
    ): (T) -> Boolean{
        return { this(it) && other(it) }
    }
    
    infix fun <T> ((T) -> Boolean).or(
            other: (T) -> Boolean
    ): (T) -> Boolean{
        return { this(it) || other(it) }
    }
    
    operator fun <T> ((T) -> Boolean).not(): (T) -> Boolean = {!this(it)}
    fun String.isUpperCase() = all { it.isUpperCase() }
    fun isShort(s: String) = s.length <= 4
    fun main() {
        val data = listOf("abc", "ABCDE", "ababc", "BdAc", "xyz")
        println(data.count(::isShort and String::isUpperCase)) // infix fun .and 사용
        // 0
        println(data.count(::isShort or String::isUpperCase)) // infix fun .or 사용
        // 4 >> abc, ABCDE, BdAc, xyz
        println(data.count(!::isShort and String::isUpperCase)) // infix fun .and 사용
        // 1 >> ABCDE
        println(data.count(!(::isShort and String::isUpperCase))) // infix fun .or 사용
        // 5 >> abc, ABCDE, ababc, BdAc, xyz
    }

    5) 대입

    다른 이항 연산으로 +=와 같은 복합 대입 연산이 있다.

    가변 컬렉션과 불변 컬렉션에 따라 복합 대입 연산의 동작이 달라진다.

    +=를 불변 컬렉션 타입의 변수에 적용하면 새로운 컬렉션 객체가 생기고

    이 객체를 변수에 대입해 변수 값이 바뀐다. 그래서 이 경우 변수가 가변이어야 한다.

    var numbers = listOf(1, 2, 3)
    numbers += 4
    println(numbers) // [1, 2, 3, 4]

    말이 어려워서 혼자 이것저것 해봤더니, 불변 컬렉션(immutable collection)의 경우(위 코드에서 list)

    +=을 var 타입에 변수에 대입하면, 컬렉션은 불변 컬렉션이므로 

    기존 값 + 추가한 값을 가진 새로운 컬렉션을 만든 뒤 numbers에 대입한다.

    (즉 numbers가 가리키고 있는 변수의 주소가 바뀐다)

    이 때 numbers는 val 타입의 변수인 경우 변경이 불가능하므로, var 타입의 변수여야만 한다.

    val numbers1 = listOf(1, 2, 3)
    numbers1 += 4 // ERROR : Val cannot be reassigned

    반대로 가변 컬렉션의 경우 += 연산을 컬렉션에 적용하면, 컬렉션의 내용은 바뀌지만

    객체 자체의 정체성(가리키고 있는 주소)는 바뀌지 않는다.

    var numbers1 = listOf(1, 2, 3)
    println(numbers1) // ref : @875
    numbers1 += 4
    println(numbers1) // ref : @890
    // numbers1이 가리키는 주소가 바뀌는것을 확인
    
    val numbers2 = mutableListOf(1, 2, 4)
    println(numbers2) // ref : @ 893
    numbers2 += 5
    println(numbers2) // ref : @ 893
    // numbers2이 가리키는 주소가 동일한 것을 확인

    임의의 타입에 대해 위 두 가지 관습(convention)을 적용할 수 있다.

    += 같은 연산자를 복합 대입 연산자(augmented assignment operator)라고 부르며

    정해진 관습에 따라 연산자에 대응하는 함수표를 작성해야 한다.

    의미 의미
    a += b a = a.plus(b) a.plusAssign(b)
    a -= b a = a.minus(b) a.minusAssign(b)
    a *= b a = a.times(b) a.timesAssign(b)
    a /= b a = a.div(b) a.divAssign(b)
    a %= b a = a.rem(b) a.remAssign(b)

    복합 대입 연산자를 해석하는 방법은 다음과 같다.

     

      ● 커스텀 복합 대입 함수가 있다면 그 함수를 사용

        ○ 복합 대입 함수의 존재 여부에 따라 복합 대입문을 대응하는 복합 함수로 변환해 컴파일

        ○ (+=의 경우 plusAssign()이 있는지, -=의 경우 minusAssign()이 있는지 등)

     

      ● 복합 대입 연산자 함수가 없는 경우(plusAssign() 등) 복합 대입문을

          이행 연산자 + 대입을 사용한 연산으로 해석

        ○ a + b의 경우 plus()가 있으면 a = a.plus(b)로 해석

     

      ● 복합 대입 연산자의 왼쪽 피연산자가 불변인 경우 변수에 새 값을 대입할 수 없으므로

          a = a.plus(b)와 같이 일반 대입문과 이항 연산을 활용한 방식으로 해석이 불가능


    다음글

    11-1-2.  도메인 특화 언어 : 연산자 오버로딩(6~8)

    반응형

    댓글

Designed by Tistory.