LHJ

I'm a FE developer.

18. Abstract Loop & Lazy Execution

11 Aug 2020 » codespitz_re

Abstract Loop & Lazy Execution

Generator 는 동기명령을 중간에 멈출 수 있는 기능이 있다.
yield(이러한 기능을 suspension이라 한다.)
그리고 다시 재진행한다.

이것이 기본사항이돼서 이를 바탕으로 루프를 추상화(abstract loop)한다.
그리고 지연실행(lazy execution)을 배울 것이다.

지연실행이라는 것은 함수의 특권이다.
여러분이 어떤 제어문을 작성했는데, 그 문이 즉시실행되지 않게 하려면 원래는 함수에 담아두고 나중에 실행한다.
그럼 실행될 때까지 해당 ‘문’은 지연된다.

기본적으로 지연실행이라는 것은 함수호출 지연실행인데, 이걸 함수의 호출지연말고도 Generator를 통해서도 지연시킬 수 있다.
Generator 는 문이 다 실행되지 않고 빠져나올 수 있기 때문에, yield 밑에를 실행시키지 않고 지연시킬 수 있다.

기존 지연실행이 함수형 프로그래밍의 특징이자 함수형의 특권으로 여겨졌다면,
코루틴을 지원하는 대부분의 언어에서는 제어문으로 지연실행을 일으킬 수 있다.
기존의 복잡한 함수형 프로그래밍 지연 실행을 지금은 다 제어문으로 실행할 수 있는 것이다.

Abstract Loop

루프의 추상화

아니 루프가 루프지 루프를 추상화해? 라고 생각할지도 모르지만, 이미 지난 시간에 iterator 를 통해 루프를 추상화하는 방법을 공부했다.
루프문을 iterator 객체로 바꿔놓으면 루프를 위한 상태값들을 iterator 객체가 가지고 있기 때문에 언제든지 똑같은 루프를 재현할 수 있게 되고, 루프문의 역할 또한 크게 줄어드는 것을 알 수 있었다.

기존의 제어문은 많은 역할이나 책임을 갖고 있었다.
반면 iterator 를 사용한 경우는, 루프문을 더 돌릴지말지, 루프마다 무얼할지, 이런 결정권들이 iterator 객체로 넘어가기 때문에 훨씬 더 여러번 똑같은 루프를 제어할 수 있는 객체를 만들어낼 수 있다.

이러한 장점을 본격적으로 이용해보려고 하는 것이다.
이전 시간까지는 이것이 진짜 제어문에 비해서 좋은건가?
이런 의문이 드는 상태였다면 이번시간에 좀 더 복잡한 루프를 해결해보자는 것이다.

Complex Recursion - 1. 단순 배열루프

단순한 배열 루프부터 시작하자.

{
    [Symbol.iterator](){return this;},
    data:[1, 2, 3, 4],
    next(){
        return {
            done: this.data.length == 0,
            value: this.data.shift()
        }
    }
}

iterable 과 iterator interface 를 떠올려보자.

  1. iterable interface 는 Symbol.iterator 메서드를 가지고 있다.
  2. iterable interface 는 iterator 를 반환한다.
     [Symbol.iterator](){return this;}
    
  3. iterator interface 조건은 반드시 next 메서드를 가지고 있어야 한다는 것이다.
    위 예시의 객체 자체가 next 메서드를 가지고 있으므로 thisreturn해도 iterable 규약에 어긋나지 않는다.
  4. next를 호출하면 iterator result object를 반환한다.
    iterator result object의 조건은 done, value가 있어야 한다는 것이다.
     {
         done: this.data.length == 0,
         value: this.data.shift()
     }
    

Complex Recursion - 2. 복잡한 다층형 그래프는 어떻게 iteration 할 것인가?

복잡한 다층형 그래프는 어떻게 iteration 할 것인가?

{
    [Symbol.iterator](){return this;},
    data: [{a:[1,2,3,4], b:'-'}, [5,6,7], 8,9],
    next(){
        return ???;
    }
}

위와 같이 생기면 어떻게 할 것인가?
ES6 이전과 이후의 객체 리터럴은 차이가 있다.
바로 순서이다.
ES6 이전엔 객체 리터럴에는 순서가 없다.
순서가 없다는 것은 자바로 따지면 해시맵으로 되어있다는 것이다.
그런데 자바스크립트 ES6 이후부턴 객체 리터럴을 통해 객체를 선언하면 링크드 해시맵으로 되어있다.
그렇기 때문에 순서가 있는 것이다.

for ... in으로 반복시키면 반드시 순서대로 나온다.

현재 목표

위의 객체의 data를 해체하여 next 메서드로 1,2,3,4,-,5,6,7,8,9 값을 내보내게 하는 것이 목표이다.

const a = {
    [Symbol.iterator](){return this;},
    data: [{a:[1,2,3,4], b:'-'}, [5,6,7], 8,9],
    next(){
        let v;
        while(v = this.data.shift()) {
            switch (true) {
                case Array.isArray(v):
                    this.data.unshift(...v);
                    break;
                // null 이 아니면서 객체인 경우
                case v && typeof v == 'object':
                    let n = [];
                    for (var k in v) n.push(v[k]); 
                    this.data.unshift(n);
                    break;
                default:
                    return {value:v, done:false};
            }
        }
        return {done: true};
    }
}

for (const v of a) console.log(v);

// 1
// 2
// 3
// 4
// -
// 5
// 6
// 7
// 8
// 9

위 코드 풀이

  1. data : [ {a: [1,2,3,4], b:’-‘}, [5,6,7], 8,9 ]
  2. v = this.data.shift() : 위 data 에서 맨 앞에 값 추출 - {a: [1,2,3,4], b: ‘-‘}
  3. case v && typeof v :추출된 맨 앞의 값은 Object 이므로 두번째 case 에서 조건이 일치한다.
  4. for (var k in v) n.push(v[k]) : n 변수에 [[1,2,3,4], ‘-‘] 배열이 담기게 된다.
  5. this.data.unshift() : data 앞 부분에 [[1,2,3,4], ‘-‘] 를 넣는다.
  6. 현재 data 상태 : [[[1,2,3,4], ‘-‘], [5,6,7], 8,9]

  7. v = this.data.shift() : [[1,2,3,4], ‘-‘]
  8. Array.isArray(v) : true
  9. data.unshift(…v) : data의 상태 - [[1,2,3,4], ‘-‘, [5,6,7], 8,9]

  10. v = this.data.shift() : [1,2,3,4]
  11. Array.isArray(v) : true
  12. data.unshift(…v) : [1,2,3,4, ‘-‘, [5,6,7], 8,9]

  13. v = this.data.shift() : 1 // data 상태 [2,3,4, ‘-‘, [5,6,7], 8,9]
  14. return { value: v, done: false} : 1

  15. v = this.data.shift() : 2 // data 상태 [3,4, ‘-‘, [5,6,7], 8,9] … …

아직까지 예제로 보여주고있는 iterable 객체는 내부에 data가 있어 한번 루프가 돌고나면 사라지는 형태다.
iterable로 data 사본을 만들 수 있는 ‘기회’가 생긴다고 했지 아직 내가 보여준 예제들은 그렇게 만들진 않았다.

여튼 조금 더 어려운 형태의 루프이다.
기존 while, for문 처럼 i=0 이런 조건이 아닌, data에 대한 판정이 들어가있다.
그리고 이런 data에 대한 판정Run Time에서 계속 바뀐다.

왜?

data의 삽입이 일어나기 때문이다.

data의 삽입이란 위 루프문으로 data가 들어간다는 뜻이다.
위의 예제는 객체 안에 data를 넣어놨지만 실제로는 위 iterable 객체를 돌릴 때마다 새로운 data가 삽입된다.
그렇기 때문에 Run Time에서 data에 대한 판정이 계속 바뀐다는 것이다.

이렇기 때문에 max 조건같은 안전장치를 하나 걸어야된다.
어떤 data 값이 삽입될지 모르기 때문이다.
어떤 data 값이 삽입될지 모르기 때문에 Run Time 평가를 하면 루프문이 무한히 돌 가능성이 있다.

여튼 위와 같이 소스를 만들면 data 형태가 어떻게 들어오든 전부 해체해버린다.
그런데 이런식으로 소스를 구성하면 컴퓨터가 느리지 않을까? 라는 걱정은 안해도된다.
컴퓨터는 반복의 제왕이다.
우리는 컴퓨터의 반복을 이길 수 없다.
여러분들이 어지간히 코드를 개선해도 10만번 돌려서 1/1000 밀리초 땡기기도 쉽지 않다.
컴퓨터는 원래 너무 빠르다.
여러분들의 비효율성을 흡수할 수 있을 정도로 빠르다.

이러한 이유로 알고리즘에 너무 욕심을 안내도된다.
컴퓨터 프로그래머 초보가 가장 많이 하는 실수가 바로 어떻게하면 로직을 하나라도 더 줄일 수 있을까에 대한 노력을 많이 하는 것이다.
알고리즘 퀴즈에선 중요할지몰라도 실무에서는 문제를 더 정확하게 해결하는 것이 중요하다.

코드스피츠 73 확인 필요
위의 식에 오류가 있다고 한다.
그 오류를 알아내서 수정하는 것이 과제다.
힌트 : data의 a의 값을 3 또는 7과 같은 값으로 바꾸면 알 수 있다고 한다.
…이 다음 강의 때 if (v.hasOwnProperty())를 안 넣어준 것이 실수라고…


위의 코드는 복잡한 것들 중에선 가장 단순한 것이다.
위의 코드가 조금만 더 복잡해지면 문제가 발생한다.
디버깅이 안된다.
디버깅을 해도 루프를 조금 돌다가 걸린다.
또한 생각할 조건이 더 많아지면 조건문은 어떻게 처리할 것인가?
case가 더 많아진다면 어떻게 처리할 것인가?
사람이 감당할 수 있을까?
아무도 감당 못한다.
즉, 코드를 위와 같이 짤 수는 없다는 것이다.

위의 복잡한 코드를 정리하자

{
    [Symbol.iterator](){return this;},
    data: [{a:[1,2,3,4], b:'-'}, [5,6,7], 8,9],
    next(){
        let v;
        // 아래에 안전장치를 걸어줘야 한다!! max 같은!!
        while(v = this.data.shift()) {
            // Object의 instance가 아닌 애들 : Number, String, Boolean, NaN, undefined 즉, primitive value 원시값(기본값)
            // null은 안타깝게도 Object - ES6에서도 과거와의 호환성 때문에 수정 안함
            // 그렇기 때문에 앞에 !v && 조건을 더 붙인 것 - falsy 인 값들은 Object일리가 없기 때문에 이렇게만 적으면 된다.
            // 첫줄 if문의 return값에 done 키 안 넣음 -> undefined. done은 truthy, falsy 값이면 된다.
            // 첫줄에서 원시값(기본값) 처리
            if (!v && !(v instanceof Object)) return {value:v};
            // 두번째 줄에서 배열인지 아닌지를 판정
            // 배열이 아닌 애들을 먼저 솎아낸다. 
            // 왜? 보다 추상 계층인 애를 먼저 보는 것이다. 구상 계층은 밑에 내릴려고..
            // 배열이 아닌 애들은 Object.values라는 ES6 이후 도입된 메서드를 사용해 Object에서 value만 모아서 배열로 만들어준다.
            if (!Array.isArray(v)) v = Object.values(v);
            // 배열이 아닌것도 위에서 Object.values 메서드로인해 다 배열로 바뀌었으므로 
            // 아래 식에서 다시 해체 할당된다.
            this.data.unshift(...v);
        }
        return {done: true};
    }
}
{
    [Symbol.iterator](){return this;},
    data: [{a:[1,2,3,4], b:'-'}, [5,6,7], 8,9],
    next(){
        let v;
        // 아래에 안전장치를 걸어줘야 한다!! max 같은!!
        while(v = this.data.shift()) {
            if (!v && !(v instanceof Object)) return {value:v};
            if (!Array.isArray(v)) v = Object.values(v);
            this.data.unshift(...v);
        }
        return {done: true};
    }
}
1. data                                 : [{a:[1,2,3,4], b:'-'}, [5,6,7], 8,9]
2. v = this.data.shift()                : {a:[1,2,3,4], b:'-'}
3. !Array.isArray(v)                    : true
4. v = Object.values(v)                 : v 상태 - [[1,2,3,4], '-']
5. this.data.unshift(...v)              : data 상태 - [[[1,2,3,4], '-'], [5,6,7], 8,9]

6. v = this.data.shift()                : [[1,2,3,4], '-']
7. this.data.unshift(...v)              : data 상태 - [[1,2,3,4], '-', [5,6,7], 8,9]

8. v = this.data.shift()                : [1,2,3,4]
9. this.data.unshift(...v)              : data 상태 - [1,2,3,4, '-', [5,6,7], 8,9]

10. v = this.data.shift()               : 1 // data 상태 - [2,3,4, '-', [5,6,7], 8,9]
11 !v && !(v instanceof Object)         : true
12. return { value : v }                : 1 // done 은 falsy 값으로 전달했기 때문에 false

13. v = this.data.shift()               : 2 // data 상태 - [3,4, '-', [5,6,7], 8,9]
...