코틀린을 다루는 기술 9장을 읽고 정리한 내용입니다.
요약된 표현이 많기에 보다 자세한 설명은 책을 통해 확인하실 수 있습니다.

예제도 이해하기가 어렵고 점점 정리하기가 어려워져간다... 모든 api를 while문 대신 재귀 호출로 작성하는 도서가 있다?! 이 책의 재귀 사랑은 멈출 기세가 보이지 않는데...!!

9.1 즉시 계산과 지연 계산

즉시 계산 언어는 메서드나 함수의 인자를 평가할 때는 즉시 계산을 사용한다. 하지만 다른 구성 요소는 지연 계산이다.

val x = getValue()
  • 즉시 계산 언어 : x의 참조가 선언되자마자 getValue() 함수가 호출되어 값을 할당한다. 즉 식이나 값을 작성하자마자 계산한다.
  • 지연 계산 언어 : x에 의해 참조되는 값이 쓰이는 시점에 getValue()가 호출된다. 식의 값이 필요할 때 그 값을 계산한다.

코틀린에서도 지연 계산이 필요한 때도 있다. 예를 들자면 아래와 같은 if-else 구문이 있다고 하자.

val result =
    if (condition()) {
        getTrueValue()
    } else {
        getFalseValue()
    }

condition() 는 항상 실행되지만, 이 함수의 결과에 따라서 getTrueValue() 와 getFalseValue() 중에서 하나만 호출된다.

  • 만약 이 코드가 완전한 즉시 계산을 사용하는 구조였다면 if-else 와 관계 없이 두 함수가 모두 처리되었을 것이다.
  • 반대의 경우였다면 getTrueValue()와 getFalseValue()가 둘 다 계산된 후에 condition()에 따라 한 쪽 값을 반환하기 때문에 처리 시간이 더 오래 걸릴 것이다.

9.2 코틀린과 즉시 계산

다음은 코틀린이 지연 계산을 적용하는 요소의 예다.

  • ||와 &&
  • if - else
  • for, while
  • try - catch

||와 & 연산자는 결과를 계산할 때 필요 없는 피연산자를 계산하지 않는다.

getFirst() || getSecond()

만약 getFirst()가 true를 반환한다면 두번째 피연산자인 getSecond()의 연산은 생략된다.

or(getFirst(), getSecond())
fun or(a: Boolean, b: Boolean) = if (a) true else b

반면 이 or 함수는 첫번째 인자의 값이 무엇이든간에 무조건 두번째 인자를 평가한다.
6장, 7장에서 만들었던 getOrElse()함수도 동일한 방식으로 주 계산에 성공하더라도 불필요하게 기본 인자를 즉시 계산하게 된다.

9.3 코틀린과 지연 계산

코틀린은 기본적으로 즉시 계산 언어지만, 항상 즉시 계산만 사용하는 것은 아니다. 만약 지연 계산을 사용하지 않는다면, try - catch 구문에서는 예외가 발생하지 않더라도 catch 블록이 무조건 실행될 것이다.

오류에 대한 처리를 위해 지연 계산이 필수인 것처럼, 무한한 데이터 구조를 조작하고 싶을 때도 지연 계산이 필요하다. 코틀린에서는 위임 패턴을 사용해 지연 계산을 구현한다.

위임 패턴(delegation pattern) : 한 객체가 기능 일부를 다른 객체로 넘겨주어, 첫번째 객체 대신 수행하도록 한다. 다른 클래스의 기능을 사용하되 그 기능을 변경하지 않기 위해 위임 패턴을 사용한다.

Delegated properties

Kotlin supports delegated properties:
The syntax is: val/var <property name>: <Type> by <expression>. The expression after by is the delegate, because get() (and set()) corresponding to the property will be delegated to its getValue() and setValue() methods. Property delegates don’t have to implement any interface, but they have to provide a getValue() function (and setValue() — for vars)

하나의 클래스를 다른 클래스에 위임하도록 선언해서, 위임된 클래스가 가지는 인터페이스 메소드를 참조 없이 호출할 수 있게 해주는 기능이다.

class Example {
    val p: String by Delegate() // 이 프로퍼티의 get/set을 Delegate에 위임
}

class Delegate {

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, thank you for delegating '${property.name}' to me!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value has been assigned to '${property.name}' in $thisRef.")
    }
}

fun main() {
    val first = Example()
    println(first.p) // search.Example@6ae40994, thank you for delegating 'p' to me!
}

Lazy

lazy()는 인스턴스가 Lazy를 구현하는 함수를 반환한다. 첫번째 get() 호출은 계산 결과를 기억하고 있다가, 재호출시 기억해놨던 결과를 다시 반환한다.

val example: Int by lazy {
    println("ㅎㅇ")
    3
}

fun main() {
    println(example)
    println(example)
}
ㅎㅇ
3
3

하지만 아래와 같은 코드는 진정한 지연 계산이 아니다. a와 b를 초기화할 때 사용한 람다는 참조가 선언될 때 호출되지 않는다. or 함수의 인자로 a, b가 전달될 때는 함수 인자를 즉시 계산하므로 b에 연결된 람다도 실행된다.

fun main() {
    val a = lazy { true }
    val b = lazy { throw IllegalArgumentException() }
    println(or(a, b)) // error!
}

fun or(a: Boolean, b: Boolean) = if (a) true else b

9.4 지연 계산 구현

코틀린에서 지연 계산을 완벽하게 구현하는 것은 불가능하다. 하지만 제대로 된 타입을 사용하면 같은 목표를 달성할 수 있다.

지난 장에서 Option과 Result라는 타입을 만들어 어떤 종류의 모드를 부여했다. 지연 계산을 또다른 모드라 생각해 이를 Lazy<T>라는 타입으로 구현할 수 있다.

지연 정숫값을 만들려면 다음과 같이 쓰면 된다.

val x: () -> Int = { 5 }

지연 계산에 맞춰 or 함수를 새로운 타입에 맞춰 바꿔 쓰고, ||의 두 연산자도 함수 호출을 사용해 값을 계산하게 바꿔야 한다. 또한 시간이 오래 걸리는 연산을 최초 1회만 계산해두고, 재계산을 생략할 수 있도록 필요에 의한 호출을 구현한다.

class Lazy<out A>(f: () -> A) : () -> A {
    private val value: A by lazy(f)

    override operator fun invoke(): A = value
}

함수 끌어올리기

인자 : 두개의 함수를 인자로 받는 커리한 함수

반환 값 : 지연 계산 값을 받아 지연 계산 값을 반환하는 함수를 반환하는 함수 (문장 실화?)

즉, (Lazy<T>) → (Lazy<T>) → Lazy<T>를 처리하는 함수를 작성한다.

companion object {

        fun <A, B, C> lift2(f: (A) -> (B) -> C): (Lazy<A>) ->  (Lazy<B>) -> Lazy<C> =
            { ls1 ->
                { ls2 ->
                    Lazy { f(ls1())(ls2()) }
                }
            }
    }

Lazy 값 매핑하고 펼치기

Lazy 클래스의 인스턴스로 map, flatMap 정의하기

→ 계산이 즉시 일어나지 않게 인자로 받은 함수를 새로운 Lazy 객체로 만들어 감싼다.

class Lazy<out A>(f: () -> A) : () -> A {
    private val value: A by lazy(f)

    override operator fun invoke(): A = value

    fun <B> map(f: (A) -> B): Lazy<B> = Lazy { f(value) }

        fun <B> flatMap(f: (A) -> Lazy<B>): Lazy<B> = Lazy { f(value)() }

    companion object {

        fun <A, B, C> lift2(f: (A) -> (B) -> C): (Lazy<A>) -> (Lazy<B>) -> Lazy<C> =
            { ls1 ->
                { ls2 ->
                    Lazy { f(ls1())(ls2()) }
                }
            }
    }
}

Lazy와 List 합성하기

List<Lazy>를 Lazy<List>로 변환해 리스트를 A를 인자로 받는 함수와 지연 합성시킨다.

원소를 각각 평가하는 값을 가지고 리스트를 매핑하면 된다. 값을 즉시 평가하지 않도록 map 연산을 새로운 Lazy 객체로 감싸야 한다.

예외 처리를 고려해 지연 연산을 처리하고 싶다면, 이전 장에서 만든 Result를 타입으로 사용하는 sequenceResult를 사용한다.

fun <A> sequence(lst: List<Lazy<A>>): Lazy<List<A>> = Lazy { lst.map { it() } }

fun <A> sequenceResult(lst: List<Lazy<A>>): Lazy<Result<List<A>>> =
        Lazy { sequence(lst.map { Result.of(it) }) }

9.5 추가 지연 합성

함수를 지연 계산으로 합성하는 것은 일반적인 합성을 Lazy로 감싸기만 하면 된다. 어떤 구현이든 함수 값으로 감싸서 지연 계산으로 만들면 된다.

효과를 지연 계산으로 적용하기

Lazy를 호출해 값을 얻고, 그 값에 효과를 적용하면 된다.

조건과 두개의 효과를 받아서 조건이 true일 때 Lazy의 첫번째 효과를, false일 때 두번째 효과를 적용하는 forEach함수를 작성해보자

class Lazy<out A>(function: () -> A): () -> A {

    private val value: A by lazy(function)

    override operator fun invoke(): A = value

    fun <B> map(f: (A) -> B): Lazy<B> = Lazy { f(value) }

    fun <B> flatMap(f: (A) -> Lazy<B>): Lazy<B> = Lazy { f(value)() }

    fun forEach(condition: Boolean, ifTrue: (A) -> Unit, ifFalse: () -> Unit = {}) =
            if (condition)
                ifTrue(value)
            else
                ifFalse()

    fun forEach(condition: Boolean, ifTrue: () -> Unit = {}, ifFalse: (A) -> Unit) =
            if (condition)
                ifTrue()
            else
                ifFalse(value)

    fun forEach(condition: Boolean, ifTrue: (A) -> Unit, ifFalse: (A) -> Unit) =
            if (condition)
                ifTrue(value)
            else
                ifFalse(value)

    companion object {

        fun <A, B, C> lift2(f: (A) -> (B) -> C): (Lazy<A>) ->  (Lazy<B>) -> Lazy<C> =
            { ls1 ->
                { ls2 ->
                    Lazy { f(ls1())(ls2()) }
                }
            }
    }
}

한쪽이 참 또는 거짓일 때만 값을 사용하는 경우를 고려해서, 세가지 버전으로 forEach를 작성한다.

지연 계산이 없으면 할 수 있는 일

아래와 같은 알고리즘을 구현해야 한다 가정해보자.

  1. 양의 정수만 존재하는 리스트를 얻는다
  2. 그중 소수만 남긴다
  3. 걸러낸 소수 중 앞의 10개를 취한다.

지연 계산이 없다면 이 알고리즘의 구현은 비효율적이다. 무한대에 가까운 양의 정수에 대해서 소수 여부, 원소 개수가 10개의 도달했는지 여부 등을 조사해야 한다.

이런 종류의 문제를 풀 때는 지연 리스트를 사용하는 것이 좋다.

평가하지 않은 데이터를 저장하면서 일부를 평가할 수 있고, 이미 평가된 부분을 재사용하는 Stream 클래스를 만들어보자.

추후 보충 예정