Frontend/JS

[JavaScript] L.flatMap, flatMap

턴태 2023. 5. 10. 07:18

flatMap

flatMap은 최신 자바스크립트 스펙에 등장하는 함수로, map과 flatten을 동시에 수행하는 함수다.

console.log([[1, 2], [3, 4], [5, 6, 7]].flatMap(a => a));
// (7) [1, 2, 3, 4, 5, 6, 7]

이렇게 기본 Array 프로토타입의 flatMap 메서드를 사용하면 하나의 배열로 만들어주며, 함수를 적용한다.

 

이는 배열이 아닌 값이 원소로 들어가도 동일하게 작동한다.

console.log([[1, 2], [3, 4], [5, 6, 7], 8, 9, [10]].flatMap(a => a));
// (10) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

여기서 함수를 다르게 하여 flatMap을 최대한 활용할 수 있다.

console.log([[1, 2], [3, 4], [5, 6, 7]].flatMap(a => a.map(a => a * a)));
// (7) [1, 4, 9, 16, 25, 36, 49]

각 원소에 제곱을 하여, flat과 map을 동시에 실행할 수 있다.

 

사실상 이 함수는 map을 한 배열에 flatten을 한 것과 동일한 것이다.

console.log(flatten([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a * a))));
// (7) [1, 4, 9, 16, 25, 36, 49]

flat과 map이 공존하여 정적 메서드로 존재하는 이유는 flat과 map이 비효율적으로 사용되기 때문이다.

 

위의 함수의 경우에 map을 통해 모든 값을 순회하면서 함수가 적용된 배열을 반환한다. 그리고 flatten으로 다시 순회를 하면서 배열을 담기 때문에 비효율적이다. 따라서, 이를 한 번에 수행하여 시간 복잡도를 줄이는 것이 좋다. 사실 flatMap과 flatten + map은 시간 복잡도 면에서 차이가 없다.

지연 평가가 적용된 L.flatMap

이터러블을 다룰 수 있으며, 지연평가할 수 있는 flatMap을 만들어 보고자 한다.

더보기
const isIterable = a => a && a[Symbol.iterator];

L.flatten = function *f(iter) {
  for (const a of iter) {
  	if (isIterable(a)) yield *f(a);
    else yield a;
  }
}
L.flatMap = pipe(L.map, L.flatten);
const curry = f =>
  (a, ...fs) => fs.length
  ? f(a, ...fs)
  : (...fs) => f(a, ...fs);
  
const reduce = curry((f, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]();
    acc = iter.next().value;
  }
  for (const a of iter) {
    acc = f(acc, a)
  }
  return acc;
};
  
const go = (...fs) => reduce((a, f) => f(a), fs);

const pipe = (...fs) => a => go(a, ...fs);

L.flatMap = curry(pipe(L.map, L.flatten));

이제 함수가 잘 작동하는지 테스트할 수 있다.

const it = L.flatMap(map(a => a * a), [[1, 2], [3, 4], [5, 6, 7]]);

console.log(it.next());
// {value: 1, done: false}
console.log(it.next());
// {value: 4, done: false}
console.log(it.next());
// {value: 9, done: false}
console.log(it.next());
// {value: 16, done: false}
console.log(it.next());
// {value: 25, done: false}
console.log(it.next());
// {value: 36, done: false}
console.log(it.next());
// {value: 49, done: false}
console.log([...it]);
// (7) [1, 4, 9, 16, 25, 36, 49]
const it = L.flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]);
console.log([...it]);
// (7) [1, 2, 3, 4, 5, 6, 7]

 최종적으로 바로 평가까지 완료된 함수로 사용하고자 할 때 아래와 같이 마무리할 수 있다.

const flatMap = curry(pipe(L.map, flatten));

flatMap은 원하는 함수와 배열을 인자로 넣어서 해당 함수를 적용시킨 배열을 1차원 배열인 상태로 만들 수 있다는 장점이 있다.

 

예를 들어서 [1, 2, 3]이라는 배열에 range 함수를 적용할 수 있다.

L.range = function *(l) {
  let i = -1;
  while (++i < l) yield i;
}

console.log(flatMap(L.range, [1, 2, 3]));
// (6) [0, 0, 1, 0, 1, 2]

그냥 map을 사용했다면, 2차원 배열로 저장됐을 로직을 1차원 배열을 반환하도록 변경한 것이다.

2차원 배열 다루기

2차원 배열을 flatten이나 flatMap을 통해 다뤄보자.

const arr = [
  [1, 2],
  [3, 4, 5], 
  [6, 7, 8],
  [9, 10]
];

go(arr
  console.log);
  
// (4) [Array(2), Array(3), Array(3), Array(2)]

이 코드를 flatten으로 1차원 배열로 펼쳐줄 수 있다.

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

여기에 filter를 붙여 홀수만 출력할 수도 있다.

go(arr,
  flatten,
  filter(a => a % 2),
  console.log);
  
// (5) [1, 3, 5, 7, 9]

이렇게 동작한다는 것은 지연 평가를 적용할 수 있다는 의미이기도 하다.

go(arr,
  L.flatten,
  L.filter(a => a % 2),
  takeAll,
  console.log);
  
// (5) [1, 3, 5, 7, 9]

예를 들어, [1, 2] 원소를 flatten으로 펼친 다음, 1 값이 filter를 거치고, takeAll을 거쳐서 값을 하나 꺼내 출력하고, 2도 마찬가지의 로직을 따른다.


함수 체이닝을 굉장히 많이 사용했던 강의였다. 그래서 이전에 배웠던 curry, reduce, go, pipe가 많이 재등장했다. 이전에 공부했던 함수들을 다시 꺼내어 정리하면서 도움이 많이 됐다. 예전에는 이전 게시물에서 복사 붙여넣기만 했었는데, 이젠 직접 curry, reduce, go, pipe 함수를 작성한다. 점점 더 이해가 되다 보니까 자연스럽게 쓸 수 있어 좋았다. 지금까지의 강의는 대부분 함수를 구현하고, 지연 평가로 전환하며 함수 합성에 적용해보는 것이어서 이런 것들을 연습하고, 실제 구현하는 능력도 기를 수 있는 기회인 것 같다.

 

출처: 인프런 함수형 프로그래밍과 JavaScript ES6+