LHJ

I'm a FE developer.

29. Abstract Loop & Lazy Execution - stream

14 Aug 2020 » codespitz_re

Stream

우리는 Stream 이라는 것으로 Generator 를 연결할 수 있다.
이것이 바로 자바8에 나오는 Stream의 개념이자 일반적으로 함수형 언어에서 Stream이라고 얘기하는 개념이다.
이때는 함수의 힘으로 구현하는데, 우리는 함수의 힘이 필요가 없다.
지연실행(Lazy Execution)을 제어문으로 구현할 수 있기 때문이다.
function 호출을 늦게 해서 지연시킬 필요가 없다.
Generator의 yield가 있기 때문이다.
우리는 yield를 활용하면 된다.
Generator 문만으로 지연실행을 일으켜서 Stream 을 연결할 수 있다.

const odd = function*(data) {
    for (const v of data) {
        console.log("odd", odd.cnt++);
        // 나머지 구하는 식은 양의 정수만 된다.
        // 따라서 절대값으로 계산한다.
        if (Math.abs(v) % 2) yield v;
    }
};
odd.cnt = 0;

const take = function*(data, n) {
    for (const v of data) {
        console.log("take", take.cnt++);
        if (n--) yield v; else break;
    }
};
take.cnt = 0;

for (const v of take(odd([1,2,3,4]), 2)) console.log(v);

방금 위에서 본 코드가 Stream 이다.

const Stream = class {
    static get(v) {return new Stream(v);}
    constructor(v) {
        this.v = v;
        this.filters = [];
    }
    add(gene, ...arg){
        // v를 함수의 첫번째 인자값으로 전달 - 커링
        // 제너레이터는 이터러블 객체를 반환할 수 있게 전달
        this.filters.push(v => gene(v, ...arg));
        return this;
    }
    *gene(){
        let v = this.v;
        for (const f of this.filters) v = f(v);
        // v - 배열 갯수만큼 yield를 때림
        yield* v;
    }
}

const odd = function*(data) {
    for (const v of data) if (v % 2) yield v;
};

const take = function*(data, n) {
    for (const v of data) if (n--) yield v; else break;
};

// 아래 Stream.get은 별거 없다.  
// 클래스 객체 생성할 때 new 때리기 싫어서 위에 보면 new Stream(v) 리턴한 것
// take 함수는 data와 n 인자값을 받는데, 아래 식에선 take 함수에 2라는 인자값 하나만 전달함.
// 그 이유는 위의 add 메소드에 v를 함수의 첫번째 인자값으로 전달하기 때문.
for (const v of Stream.get([1,2,3,4]).add(odd).add(take, 2).gene()) console.log(v);

// 1
// 3

위 코드 해석

  1. Stream.get([1,2,3,4]) 코드에 의해 new Stream 객체가 생성된다.
    해당 객체는 {v = [1,2,3,4], filters = []} 이렇게 생겼다.
  2. .add(odd) 를 통해 odd 함수를 add(gene, …arg)의 gene 인자로 전달한다.
  3. this.filters.push(v => gene(v, …arg)) : filters 배열에 odd([1,2,3,4]) 를 넣는다. [odd([1,2,3,4])] 그리고 return this로 {v = [1,2,3,4], filters = [odd([1,2,3,4])]}을 반환한다.

  4. odd[1,2,3,4] -> yield 1 -> .add(take(1,2)) -> this.filters = take(1, 2); -> 2는 1이되고 -> yield 1
  5. odd[1,2,3,4] -> yield 3 -> .add(take(3, 1)) -> this.filters = take(3, 1); -> 1은 0이되고 -> yield 3
  6. 마지막 .gene() 을 통해 iterable 객체를 반환
  7. 콘솔에 1, 3이 찍힌다.

음.. 위처럼 복잡하게 생각하지 말자. 아래를 보자.

// 마지막 gene() 에 의해 아래와 같은 형태인 Stream 객체에서
// this.filters 부분을 for ... of 문으로 돌려
// take() 함수 실행되고 그 후에 odd() 함수 실행되면서.. 
// Stream.get([1,2,3,4]).add(odd).add(take, 2).gene() <- 이 순서대로...
// 1, 3 모두를 yield 하여 iterable 객체로 반환하는 거 같음
{
    v : [1,2,3,4],
    filters : [
        odd(),
        take(),
    ]
}

// 즉 위에서 1, 3 iterable 객체를 반환하고 최종적으로
// for (const v of [1, 3]) console.log(v); 가 실행되는 것 같다.
// 그래서 콘솔 창에 1, 3이 찍히는 것 같다.