Primary/Kotlin

Kotlin Immutable Collections

해스끼 2022. 7. 29. 20:40
 

GitHub - Kotlin/kotlinx.collections.immutable: Immutable persistent collections for Kotlin

Immutable persistent collections for Kotlin. Contribute to Kotlin/kotlinx.collections.immutable development by creating an account on GitHub.

github.com

``List<T>``에는 리스트를 수정할 수 있는 메서드가 없지만, 어떤 리스트의 타입이 ``List<T>``라고 해서 리스트의 내용이 변경되지 않는다고 말할 수는 없다. ``MutableList<T>``를 ``List<T>``로 반환했을 지도 모르기 때문이다. Compose 컴파일러가 ``List<T>``를 unstable로 판단하는 이유이다.

 

하지만 Immutable Collections 라이브러리에는 어떠한 경우에도 내용이 변경되지 않는 collections 타입이 정의되어 있다. 내용을 변경해야 하는 경우에는 아예 새로운 객체를 만들어서 변경을 적용한 후 반환한다. Compose도 이것만큼은 stable로 인정한다.

구조

``Immutable~`` 인터페이스는 완벽한 읽기 전용이다. 아예 함수조차 ``get``과 ``subList`` 뿐이다. ``Persistent~`` 인터페이스는 리스트의 수정을 지원하지만, 리스트의 사본에 변경 사항을 적용하여 반환하기 때문에 원래 리스트는 바뀌지 않는다. 

 

딱 보면 답 나오지? ``Immutable``는 UI 등 앞단에서, ``Persistent``는 데이터를 관리하는 뒷단에서 사용하면 되겠다. UI에서는 데이터를 읽기만 하니까(정확히는 읽기만 해야 하니까). 반면 뒷단에서는 현재 데이터에 변경 사항을 적용하여 새로운 데이터를 제공해야 하는데, 이때 ``Persistent`` 인터페이스에 정의된 수정 함수를 사용하면 된다. 

코드

이 글에서는 ``ImmutableList``와 ``PersistentList``만을 살펴본다.

 

``ImmutableCollection``은 단순한 marking interface이다. 진짜 한 줄밖에 없다.

``ImmutableList``에는 ``subList()`` 단 하나의 함수만 정의돼 있다. 코드를 안 봐도 알 것 같은..

 

그런데 ``subList()``의 반환 타입인 ``SubList<T>``는 볼 만하다. 그냥 ``ImmutableList``를 잘라서 반환하는 거 아닌가 싶지만 그 말대로라면 애초에 ``SubList`` 타입이 존재할 이유가 없다. ``SubList``에서 주목할 점은, 특정 리스트의 일부만을 표현함에도 불구하고 전체 리스트를 들고 있다는 점이다.

public interface ImmutableList<out E> : List<E>, ImmutableCollection<E> {

    // 리스트 전체를 파라미터로 넘긴다.
    fun subList(fromIndex: Int, toIndex: Int): ImmutableList<E> = SubList(this, fromIndex, toIndex)

    private class SubList<E>(private val source: ImmutableList<E>, private val fromIndex: Int, private val toIndex: Int) : ImmutableList<E>, AbstractList<E>() {
        private var _size: Int = 0

        init {
            ListImplementation.checkRangeIndexes(fromIndex, toIndex, source.size)
            this._size = toIndex - fromIndex
        }

        override fun get(index: Int): E {
            ListImplementation.checkElementIndex(index, _size)

            return source[fromIndex + index]
        }

        override val size: Int get() = _size

        override fun subList(fromIndex: Int, toIndex: Int): ImmutableList<E> {
            ListImplementation.checkRangeIndexes(fromIndex, toIndex, this._size)
            return SubList(source, this.fromIndex + fromIndex, this.fromIndex + toIndex)
        }
    }
}

보다시피 원본 리스트를 계속 참조한다. (의미상) 한번 자른 ``SubList``를 잘라도 여전히 똑같은 원본을 참조한다. 리스트를 매번 자르는 게 더 비효율적이라고 판단한 듯하다. 흠.. 나름 일리 있는 것 같기도 하고?

 

이제 ``PersistentCollection``을 보자. ``PersistentCollection``에는 몇 가지 수정 함수와, 수정 함수를 지원하기 위한 ``Builder`` 인터페이스가 정의돼 있다.

public interface PersistentCollection<out E> : ImmutableCollection<E> {
    fun add(element: @UnsafeVariance E): PersistentCollection<E>

    fun addAll(elements: Collection<@UnsafeVariance E>): PersistentCollection<E>

    fun remove(element: @UnsafeVariance E): PersistentCollection<E>

    fun removeAll(elements: Collection<@UnsafeVariance E>): PersistentCollection<E>

    fun removeAll(predicate: (E) -> Boolean): PersistentCollection<E>

    fun retainAll(elements: Collection<@UnsafeVariance E>): PersistentCollection<E>

    fun clear(): PersistentCollection<E>

    /**
     * 수정 연산용 builder
     */
    interface Builder<E>: MutableCollection<E> {
        fun build(): PersistentCollection<E>
    }

    fun builder(): Builder<@UnsafeVariance E>
}

전부 이름만 봐도 알 수 있는 함수이다. 특이한 점은 모든 수정 함수가 ``PersistentCollection``을 반환한다는 점. 말했다시피 원본 객체에 수정 연산을 적용한 사본을 반환하기 때문이다.

 

단순한 삽입/삭제보다 더 복잡한 연산을 하고 싶다면, ``PersistentCollection.mutate()`` 확장 함수를 사용하자. ``mutate()`` 함수 안에서는 현재 리스트의 사본을 ``MutableList``로 접근할 수 있어 다양한 수정 연산을 적용할 수 있다. 당연히 원본은 바뀌지 않는다.

// list의 타입은 MutableList이다.
persistentList.mutate { list -> list.replaceAll { number -> number * number } }

 

``PersistentList``는 ``PersistentCollection``과 똑같은 형태의 인터페이스라 생략한다. 밑단의 세부적인 구현은 당연히 생략.

 

요약

  • 데이터를 읽기만 한다면 ``Immutable...``을 사용하자.
  • 데이터를 수정한다면 ``Persistent...``를 사용하자.

Compose 개발자들은 stability 때문에라도 자주 사용할 듯하다.