JavaScript

[FP&ES6+] L.flatMap, flatMap

_sweep 2021. 12. 28. 15:29

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

 

 

Array.prototype.flatMap과 flatten + map의 비교

Array.flatMap()

먼저 Array.map()을 사용해 각 요소에 대해 map 연산을 수행한 후 얻은 결과를 값을 펼쳐 새로운 배열로 만든다.

 

arr.flatMap(callback(currentValue[, index[, array]])[, thisArg])
  • currentValue : 배열에서 처리되는 현재 엘리먼트

 

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

// output
// [1, 2, 3, 4, 5, 6, 7, 8]
// [11, 12, 13, 14, 15, 16, 17]

 

내장 함수인 flatMap의 동작은 위와 같다.

그리고 이를 이전에 구현한 flatten과 map을 이용해 똑같은 동작을 하도록 구현할 수 있다.

 

console.log(flatten([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a + 10))));

// output
// [11, 12, 13, 14, 15, 16, 17]

 

동작하는 과정은 위의 flatMap과 같다.

하지만 flatten + map은 비효율적이다.

flatten을 하기 전 map으로 주어진 iter의 값들을 새로운 값들로 만들고 또다시 flatten으로 iter의 전체를 순회하면서 값을 펼치기 때문이다.

그래서 flatMap을 사용하면 조금 더 효율적으로 동작하지만 사실 시간복잡도 면에서는 크게 다르지 않다.

자바스크립트는 기본적으로 지연성있게 동작하지 않기 때문에 필요한 값이 하나여도 iter의 모든 값을 순회하기 때문이다.

 

 

L.flatMap

 

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

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

console.log([...it]);

// output
// [11, 12, 13, 14, 15, 16, 17]

 

flatMap을 지연성있게 동작하기 위해서는 L.map과 L.flatten을 사용하면 된다.

기본적으로 두 함수 모두 이터러블을 인자로 받아 사용하므로 자연스럽게 L.flatMap도 이터러블 프로토콜을 따르게 된다.

따라서 원하는 시점에 값을 원하는 만큼 평가할 수 있는 것이다.

 

 

flatMap

 

// 1
let flatMap = curry(pipe(L.flatMap, take(Infinity)));

// 2
flatMap = curry(pipe(L.map, L.flatten, take(Infinity)));

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

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

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

 

지연 평가를 하도록 구현한 함수로 즉시 평가를 하는 것처럼 동작하게 만들 수 있다.

이전에 L.map으로 map을 만들고 L.filter로 filter를 만들었듯이 같은 원리로 L.flatMap을 flatMap과 같이 동작하도록 만들 수 있다.

 

1에서는 L.flatMap을 바탕으로 flatMap을 만들었다.

2에서는 L.flatMap이 L.map과 L.flatten의 조합이므로 L.flatMap 대신 이 둘을 넣었다.

3에서는 L.flatten + take(Infinity)가 flatten을 구현한 것과 같으므로 이 둘 대신 flatten을 넣었다.

 

어떤 구현이든 관계없이 L.flatMap으로 즉시 평가를 하는 것처럼 flatMap을 구현할 수 있고 원하는 대로 동작하는 것을 확인할 수 있다.

 

 

✏️ 예제 - 지연성 / 이터러블 중심 프로그래밍의 실무적인 코드

 

let users = [
    {
        name: 'kiki', age: 20, family: [
            { name: 'nini', age: 25 },
            { name: 'ppippi', age: 3 }
        ]
    }, {
        name: 'hoho', age: 37, family: [
            { name: 'hana', age: 17 }
        ]
    }, {
        name: 'tata', age: 14, family: [
            { name: 'dada', age: 14 },
            { name: 'rara', age: 26 },
            { name: 'gaga', age: 29 }
        ]
    }, {
        name: 'dodo', age: 31, family: [
            { name: 'soso', age: 32 },
            { name: 'popo', age: 12 }
        ]
    },
];

go(
    users,
    L.flatMap(u => u.family),
    L.filter(u => u.age < 20),
    L.map(u => u.name),
    take(Infinity),
    console.log
);

// output
// ['ppippi', 'hana', 'dada', 'popo']

 

주어진 user 에서 각 객체의 가족들 중 나이가 20 미만인 사람들의 이름을 출력하도록 하는 코드이다.

 

이처럼 함수형 프로그래밍은 데이터를 먼저 어떻게 구성할지를 만들어내고 프로그래밍을 하는 것이 아닌 조합되어 있는 함수에 맞는 데이터를 구성하는 식으로 작성해야 한다.

 

기존 객체지향에서는 데이터를 우선적으로 정리하고 메소드를 이후에 만들며 작성해 나간다면 함수형 프로그래밍에서는 함수를 만들어두고 함수 조합에 맞는 데이터를 구성한다.