Kotlin의 asSequence

asSequence 의 특징

1. kotlin 에서 iterable을 상속한 모든 객체는 asSequence 메소드를 사용하여 sequence 형태로 변할 수 있다. 밑의 signature는 asSequence가 Iterable<T>의 확장함수로 정의되어져 있는것을 볼 수 있다

public fun <T> Iterable<T>.asSequence(): Sequence<T> {
    return Sequence { this.iterator() }
}

2. 자바에서 Stream 제공한다면 쓴다면 kotlin은 Sequence를 제공하고 거의 동일한 기능을 수행한다

3. 공통점

  1. 실행하거나 최종 연산을 하기 전까지 연산을 지연하여 함수형태로 갖는다.
  2. map, filter 등과 같은 함수형 인터페이스를 사용할 수 있다.(함수형 인터페이스 찾아보기)

차이점

제공하는 메소드가 다르다
java의 stream은 한번 수행 하고 나면 stream이 닫히게 되어 재생성 해야되는 반면 sequence는 다시 실행할 수 있다
kotlin에서 제공되는 stream을 사용할 경우, sum, fold 메소드를 사용하기 위해선 프리미티브 스트림형태로 변형해서 사용해야되는 반면, sequence는 바로 사용할 수 있다. (근데 왜 안될까 ?)
    val stream = (1..10).asSequence().asStream()
    stream.reduce{acc, s -> acc+s}.get()
    stream.fold(0){acc,s -> acc+s}  //안된다.

4. asSequence는 최종연산 메소드가 실행되기 전까지 연산을 지연한다

collection를 사용하여 메소드를 chainning 할 경우

설명 : [1,2,3,4,5]를 filter 메소드를 2번 사용하고, map을 하는 코드이다.

  val collection = intArrayOf(1,2,3,4,5).filter { it % 2 !=0 }.filter { it % 1 ==0 }.map { it + 1 }

밑의 자바코드는 위의 코틀린 코드를 decompiling 한 것이다.

map 하기 전의 두번의 filter를 하고 2번의 임시 collection을 만들고 있다. [1,2,3,4,5] 에서 홀수인 것만 filter를 한 결과값 [1, 3, 5]에 다시 1로 나누어떨어지는 수를 filter하여 [1, 3, 5] 란 결과값을 얻었고, 그것을 다시 연산 +1 을 하여 결과값 [2, 4, 6]을 만들어 내고 있다. 이 과정에서 loop 문은 총 11번 실행한 것을 확인할 수 있다.

public final class TestKt {
   public static final void main() {
      int[] $this$filter$iv = new int[]{1, 2, 3, 4, 5};
      int $i$f$map = false;
      Collection destination$iv$iv = (Collection)(new ArrayList());
      int $i$f$mapTo = false;
      int[] var6 = $this$filter$iv;
      int var7 = $this$filter$iv.length;

      int it;
      for(it = 0; it < var7; ++it) {
         int element$iv$iv = var6[it];
         int var11 = false;
         if (element$iv$iv % 2 != 0) {
            destination$iv$iv.add(element$iv$iv);
         }
      }

      Iterable $this$map$iv = (Iterable)((List)destination$iv$iv);
      $i$f$map = false;
      destination$iv$iv = (Collection)(new ArrayList());
      $i$f$mapTo = false;
      Iterator var15 = $this$map$iv.iterator();

      Object item$iv$iv;
      boolean var17;
      while(var15.hasNext()) {
         item$iv$iv = var15.next();
         it = ((Number)item$iv$iv).intValue();
         var17 = false;
         if (it % 1 == 0) {
            destination$iv$iv.add(item$iv$iv);
         }
      }

      $this$map$iv = (Iterable)((List)destination$iv$iv);
      $i$f$map = false;
      destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($this$map$iv, 10)));
      $i$f$mapTo = false;
      var15 = $this$map$iv.iterator();

      while(var15.hasNext()) {
         item$iv$iv = var15.next();
         it = ((Number)item$iv$iv).intValue();
         var17 = false;
         Integer var13 = it + 1;
         destination$iv$iv.add(var13);
      }

      List sequence = (List)destination$iv$iv;
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

sequence를 사용하여 메소드를 chainning 할 경우

asSequence를 사용할 경우, 실제로 5번의 루프를 실행하고 종료가 될것이다. 그 이유는 sequence가 filter와 map 을 사용하여 함수로써 갖고 있고, 실행하기 전까지는 함수가 실행이 되지 않기 때문이다. 최종 연산을 할때 연산을 한 후의 값을 갖게 된다. 이 방식은 함수형 프로그래밍에서 사용하는 방식으로(실제로 함수형에선 어떻게 구현되어지는지 아직은 모른다.), 객체지향언어인 코틀린에서는 실제로 구현 코드는 저런 형태로 실행되도록 구현되어져 있다.

    val sequence = intArrayOf(1,2,3,4,5).asSequence().filter { it % 2 !=0 }.filter { it % 1 ==0 }.map { it + 1 }

5. 그렇다면 위에서 이야기한 sequence가 각각 함수를 갖고 지연 실행을 어떻게 구현하고 있는지 살펴보자.

위에서 사용한 filter와 map은 밑에서 SequencesKt.filter와 SequencesKt.map으로 실행 되고 있고, 맨 처음에 SequencesKt.filter() 를 보면 시퀀스 배열 ArraysKt.asSequence(new int[]{1, 2, 3, 4, 5})과 필터에서 사용될 람다( (Function1)null.INSTANCE)를 파라미터로 넣고있다.

밑의 자바코드 또한 위의 코틀린 코드를 decompiling을 한 코드이다.

public final class TestKt {
   public static final void main() {
      Sequence sequence = SequencesKt.map(SequencesKt.filter(SequencesKt.filter(ArraysKt.asSequence(new int[]{1, 2, 3, 4, 5}), (Function1)null.INSTANCE), (Function1)null.INSTANCE), (Function1)null.INSTANCE);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

인자값으로 sequence = ArraysKt.asSequence(new int[]{1, 2, 3, 4, 5})와 predicate SequencesKt.filter()를 받고 있다. 그리고 Sequence를 상속하여 iterator과 내부 인터페이스를 구현하고 있다. iterator 메소드 안에서 iterator는 인자값으로 받은 sequence로 itertor를 만들고 있다. calcNext는 조건에 맞는지 확인해 주며 next에서 조건에 맞으면 반환하고 있다(return을 사용하여 루프를 끊어주고 있음.). 조건에 맞지 않다면 바로 넘어간다.(return에 걸리지 않음.)

위에서 filter의 코틀린을 자바로 decompiling한 내부구현 코드를 보면 밑의 코드와 같이 구현 되어져 있다.

internal class FilteringSequence<T>(
        private val sequence: Sequence<T>,
        private val sendWhen: Boolean = true,
        private val predicate: (T) -> Boolean
    ) : Sequence<T> {

        override fun iterator(): Iterator<T> = object : Iterator<T> {
            val iterator = sequence.iterator()
            var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
            var nextItem: T? = null

            private fun calcNext() {
                while (iterator.hasNext()) {
                    val item = iterator.next()
                    if (predicate(item) == sendWhen) {
                        nextItem = item
                        nextState = 1
                        return
                    }
                }
                nextState = 0
            }

            override fun next(): T {
                if (nextState == -1)
                    calcNext()
                if (nextState == 0)
                    throw NoSuchElementException()  
                val result = nextItem
                nextItem = null
                nextState = -1
                @Suppress("UNCHECKED_CAST")
                return result as T
            }

            override fun hasNext(): Boolean {
                if (nextState == -1)
                    calcNext()
                return nextState == 1
            }
        }
    }

map은 java코드로 아래와 같은 코드로 구현되어져 있고 이것도 위와 유사하게 Sequence를 상속받아 iterator를 구현하고 있다. 인자값으로는 sequence를 FilteringSequence : Sequence 타입 인자값으로 받는다. 그리고 transformer라는 람다를 받고 있다. 이곳에서의 iterator 메소드 안에서는 우리가 인자값으로 받은 sequence(FilteringSequence타입)를 사용하여 itertor()메소드를 사용하고 있다. next에서는 인자값 sequence로부터 만든 iterator 메소드로 만든 iterator(FilteringSequence타입)의 next()를 구현한다. next()는 위에서 만든 방식으로 실행이 될것이고, 인자로 받은 람다의 인자값으로 next()로 얻은 값을 넣어 수행할 것이다.

 internal class TransformingSequence<T, R>
    constructor(private val sequence: Sequence<T>, private val transformer: (T) -> R) : Sequence<R> {
        override fun iterator(): Iterator<R> = object : Iterator<R> {
            val iterator = sequence.iterator()
            override fun next(): R {
                return transformer(iterator.next())
            }

            override fun hasNext(): Boolean {
                return iterator.hasNext()
            }
        }

        internal fun <E> flatten(iterator: (R) -> Iterator<E>): Sequence<E> {
            return FlatteningSequence<T, R, E>(sequence, transformer, iterator)
        }
    }

asSequence를 사용하여 메서드를 channing 하는 방식이 위와같은 방법으로 구현되어져 있다.

내가 보면서 발견한 것은 위의 방법에는 데코레이션 패턴이 적용되어져 있다는 것이다. 매번 자신과 같은 타입을 인자값(위에서는 sequence)으로 넘기고, 각각들이 동일한 인터페이스를 다르게 구현하며, 메소드의 실행이 데코레이션에 들어간 각각의 객체들에게 채이닝 되는 현상을 볼 수 있었다.


Written by@Zero1
This blog is for that I organize what I study and my thinking, feeling and experience.

GitHub