본문 바로가기
JavaScript

[FP&ES6+] L.flatten, flatten

by _sweep 2021. 12. 27.

함수형 프로그래밍과 JavaScript ES6+ 강의를 듣고 정리한 내용입니다.

 

 

L.flatten

[[1, 2], 3, 4, [5, 6], [7, 8, 9]]와 같은 값이 들어왔을 때 결과값으로 [1, 2, 3, 4, 5, 6, 7, 8, 9] 와 같이 값을 펼친 결과를 리턴하는 함수이다.

다시 말해 위와 같은 값이 들어왔을 때 L.flatten을 사용하여 [...[1, 2], 3, 4, ...[5, 6], ...[7, 8, 9]] 처럼 동작하려 한다.

전개 연산자를 사용한 것과 같은 작업을 하기 위해서는 먼저 인자로 주어진 이터러블을 순회한다.

그러다 이터러블인 요소를 만나면 이들을 다 이터러블이 아닌 값이 될 때까지 값을 펼친다.

 

const isIterable = (a) => a && a[Symbol.iterator];

L.flatten = function* (iter) {
  for (const a of iter) {
    if (isIterable(a)) for (const b of a) yield b;
    else yield a;
  }
};

 

L.flatten은 지연성을 가지기 위해 제너레이터 함수로 만들어진다.

인자로 주어진 이터러블(iter)의 안을 순회하며 각 요소가 이터러블인지 아닌지를 isIterable 함수로 평가한다.

 

isIterable은 어떠한 값 a를 받아 a && a[Symbol.iterator]를 리턴하는 단순 동작을 한다.

a[Symbol.iterator]가 존재한다는 것은 a가 이터러블이라는 뜻이다.

다시 말하자면 a의 요소를 순회하면서 yield 키워드를 적용할 수 있다.

 

따라서 a[Symbol.iterator]가 존재하면 for of문으로 a의 요소들을 순회하며 yield 키워드를 적용해 값을 만든다.

 

var it = L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]);
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

// output
// {value: 1, done: false}
// {value: 2, done: false}
// {value: 3, done: false}
// {value: 4, done: false}

 

L.flatten은 제너레이터이기 때문에 이터레이터를 이용해 next()로 순회가 가능하다.

즉, 원하는 만큼 평가해서 원하는 만큼 값을 얻을 수 있다.

 

 

L.flatten + take로 flatten 만들기

앞서 map과 filter를 각각 L.map과 L.filter로 만들었던 것과 같은 원리로 L.flatten과 take로 flatten을 만들 수 있다.

 

const flatten = pipe(L.flatten, take(Infinity));

console.log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]));

// output
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

 

 

yield *iterable

yield *iterable은 for(const val of iterable) yield val; 와 같은 동작을 한다.

따라서 yield *iterable을 사용해 위의 L.flatten을 구현한 것을 아래와 같이 바꿀 수 있다.

 

L.flatten = function* (iter) {
  for (const a of iter) {
    if (isIterable(a)) yield *a;
    else yield a;
  }
};

 

 

 L.deepFlat

[ 1, [ 2, [ 3, 4 ], [ [ 5 ] ] ] ] 와 같이 깊은 iterable이 있다고 할 때

기존의 L.flatten의 결과값처럼 [1, 2, 3, 4, 5]를 얻어내고 싶으면 재귀함수를 사용하면 된다.

 

L.deepFlat = function* f(iter) {
    for (const a of iter) {
        if (isIterable(a)) {
            yield *f(a);
        }
        else yield a;
    }
}

console.log(take(Infinity, L.deepFlat([1, [2, [3, 4], [[5]]]])));

// output
// [1, 2, 3, 4, 5]

 

L.deepFlat을 구현할 때 제너레이터 함수의 이름을 f라고 주었다.

인자로 주어진 iter의 요소들을 순회하다 a가 이터러블이면 재귀적으로 다시 함수 f를 호출한다.

 

이 함수가 동작하는 것을 자세히 살펴보기 위해 다음과 같이 break point를 두고 동작을 살펴보았다.

 

 

iter는 [1, [2, [3, 4], [[5]]]]가 주어졌다.

이때 함수의 동작 과정은 다음과 같다.

  •  a: 1 => 이터러블이 아니므로 yield a로 바로 값으로 빠져나감
  •  a: (3) [2, Array(2), Array(1)] => 재귀 실행
  •  a: 2 => 이터러블이 아니므로 yield a로 바로 값으로 빠져나감
  •  a: (2) [3, 4] => 재귀 실행
  •  a: 3 => 이터러블이 아니므로 yield a로 바로 값으로 빠져나감
  •  a: 4 => 이터러블이 아니므로 yield a로 바로 값으로 빠져나감
  •  a: [[5]] => 재귀 실행
  •  a: [5] => 재귀 실행
  •  a: 5 => 이터러블이 아니므로 yield a로 바로 값으로 빠져나감

 

이터러블인 요소를 만나면 재귀적 호출을 통해 그 안으로 파고들어간다.

그리고 더이상 파고들어갈 곳이 없을 때 함수를 종료시켜가며 다시 위로 올라오는 과정을 거친다.

이러한 과정을 통해 [ 1, 2, 3, 4, 5 ]라는 값을 얻을 수 있다.

 

 

 

 

 

댓글