[functional JS ES6+] 지연성 2 : L.flatten, L.flatMap, 2차원 배열
작성:    
업데이트:
카테고리: Functional JS
태그: FE Language, Functional JS, JS
본 포스트는 인프런의 함수형 프로그래밍과 JavaScript ES6+ 강의(링크)를 듣고 정리한 내용입니다.
reduce와 map의 활용
queryString 만들기
객체의 정보를 이용해 queryString을 만드는 코드를 작성해보자
const queryStr = obj => go(
obj, // { limit: 10, offset: 10, type: 'notice' }
object.entries, // [["limit", 10], ["offset", 10], ["type", "notice"]]
map(([k, v]) => `${k}=${v}`), // ["limit=10", "offset=10", "type=notice"]
reduce((a, b) => `${a}&${b}`) // "limit=10&offset=10&type=notice"
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
- 참고로 obj를 받아서 첫 값이 obj 그대로 사용되므로 pipe 함수를 사용해도 된다.
const queryStr = pipe(
Object.entries, // [["limit", 10], ["offset", 10], ["type", "notice"]]
map(([k, v]) => `${k}=${v}`), // ["limit=10", "offset=10", "type=notice"]
reduce((a, b) => `${a}&${b}`) // "limit=10&offset=10&type=notice"
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
Array.prototype.join보다 다형성이 높은 join 함수
reduce 대신에 array에서 제공하는 join 내장함수 쓰면 안 되나요?
- join 함수는 array 객체를 상속하는 객체에서만 사용 가능
-
말그대로 array의 prototype에만 존재하는 join 메서드이므로
- 반면 위의 reduce는 iterable이라면 모두 사용 가능
- 더 큰 다형성을 확보 가능
그렇다면 join 함수를 더 다형성이 높게 변형해보자
const join = curry((sep = ",", iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
join('&')
);
log(queryStr({ limit: 10, offset: 10, type: 'notice' }));
- go나 pipe에서 인자 전달이 더 수월하게 하기 위해 curry 함수를 씌워서 작성
- sep(결합문자)이 별도로 없는 경우 기본으로 ‘,’ 지정
- array 뿐만이 아니라 iterable이라면 join 함수 사용 가능
-
generator로 만든 iterable도 join 함수 사용 가능
- join 이전의 함수들은 iterable protocol을 따르고 있어서 지연 가능
take와 find
- 앞서 join 함수를 reduce 함수를 통해서 제작
- 함수형 프로그래밍은 계보를 따르도록 작성 가능
- take 함수를 통해 find 함수를 제작해보자.
find 함수: iterable에서 조건을 만족하는 첫 번째 요소를 찾는 함수
즉시 평가 find
const users = [
{ age: 32 },
{ age: 31 },
{ age: 37 },
{ age: 28 },
{ age: 25 },
{ age: 32 },
{ age: 31 },
{ age: 37 },
];
const find = curry((f, iter) => go(
iter,
filter(f), // [{age: 28}, {age: 25}]
take(1), // [{age: 28}], 조건을 만족하는 요소 1개만 취함
([a]) => a)); // {age: 28} : 배열을 깨는 함수
log(find(u => u.age < 30)(users)); // {age: 28}
즉시 평가 싫어요
- 위의 함수는 하나의 결과값만 꺼내더라도 이전까지 모두 순회하며 배열을 준비하는 상황
- 이를 지연 평가를 통해 불필요한 평가를 회피해보자.
지연 평가 find
const find = curry((f, iter) => go(
iter,
L.filter(f), 🔆
take(1),
([a]) => a));
log(find(u => u.age < 30)(users));
- L.filter 함수를 통해 take부터 평가를 시작
- take 함수에게 결과 연산을 미뤄서 하나의 값이 꺼내지면 더이상의 연산을 하지 않도록 한다.
L.map과 L.filter로 map과 filter 만들기
L.map과 map
const map = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
res.push(f(a));
}
return res;
});
log(map(a => a + 10, range(4)))
위의 map 함수를 L.map을 이용해 바꿔보자.
// go 함수 기본 구조
const map = curry((f, iter) => go(
iter,
L.map(f),
take(Infinity)
));
// 초기값 iter를 첫 함수에 전달해 go 함수 축약
const map = curry((f, iter) => go(
L.map(f, iter),
take(Infinity)
));
// go 함수를 pipe 함수로 변경
const map = curry(pipe(L.map, take(Infinity)));
L.filter와 filter
// 기존 filter 함수
const filter = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
if (f(a)) res.push(a);
}
return res;
})
위의 filter 함수를 L.filter 함수를 통해 지연평가 방식으로 변경해보자.
// go 함수를 이용해 L.filter 처리
const filter = curry((f, iter) => go(
iter,
L.filter(f),
take(Infinity)
));
// 초기값 iter를 첫 함수에 전달해 go 함수 축약
const filter = curry((f, iter) => go(
L.filter(f, iter),
take(Infinity)
));
// go 함수를 pipe 함수로 변경
const filter = curry(pipe(L.filter, take(Infinity)));
L.filter 리팩토링
앞서 동작을 명확히 하기 위해 풀어쓴 L.filter 함수 코드를 리팩토링 해보자.
// 기존 L.filter
L.filter = curry(function *(f, iter) {
iter = iter[Symbol.iterator]();
let cur;
while(!(cur = iter.next()).done) {
const a = cur.value;
if (f(a)) {
yield a;
}
}
});
// 리팩토링 후
L.filter = curry(function *(f, iter) {
for (const a of iter) {
if (f(a)) yield a;
}
});
L.flatten과 flatten
L.flatten
배열 내부에 또다른 배열들이 있는 경우 이들을 모두 구조분해해서 하나의 배열로 만드는 함수
const list = [[1, 2], 3, 4, [5, 6], [7, 8, 9]];
??? // [1, 2, 3, 4, 5, 6, 7, 8, 9]
- 위와 같이 동작하기 위해 만들어진 함수이다.
- 이 함수를 만들어보자.
// a가 Symbol.iterator를 가지고 있다면 true
// a[Symbol.iterator]가 null일 수 있으므로 'a &&'로 안전하게 처리
const isIterable = a => a && a[Symbol.iterator];
L.flatten = function *(iter) {
for (const a for iter) {
if (isIterable(a)) for (const b of a) yield b;
else yield a;
}
}
const it = L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]);
log([...it]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
yield *
위의 코드를 보다 더 간단하게 바꿔보자
L.flatten = function *(iter) {
for (const a for iter) {
if (isIterable(a)) yield *a;
else yield a;
}
}
yield *iter
은for (const val of iter) yield val;
과 같다.
즉시 평가 flatten 함수
L.flatten을 즉시평가하는 함수 flatten을 L.flatten 기반으로 만들어보자
const flatten = pipe(L.flatten, take(Infinity));
log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]])); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
- flatten 함수에 배열을 인자로 전달하면 다음과 같은 순서로 진행된다.
- take 함수는 limit이 Infinity이므로 L.flatten의 yield마다 take에 추가한다.
- L.flatten은 매번 next를 진행하며 yield를 반환한다.
- 이를 반복한다.
L.deepFlat
만약 배열 내부에 또 내부 배열이 있다면?
깊은 iterable을 모두 펼치는 L.deepFlat 함수를 구현해보자.
L.deepFlat = function *f(iter) {
for (const a of iter) {
if (isIterable(a)) yield *f(a);
else yield a;
}
};
재귀적으로 내부 배열을 다시 f 함수로 넣어 yield를 내보내는 것이다.
L.flatMap, flatMap
JS 내장 flatMap 함수
flatten과 map을 동시에 실행하는 함수
- 최신 자바스크립트에 추가
- array.prototype에만 존재하는, 범용성이 떨어지는 함수
log([[1, 2], [3, 4], [5, 6, 7], 8, 9].flatMap(a => a)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
log([[1, 2], [3, 4], [5, 6, 7], 8, 9].flatMap(a => a.map(a => a * a))); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
log(flatten([[1, 2], [3, 4], [5, 6, 7], 8, 9].map(a => a.map(a => a * a)))); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
L.flatMap
- 자바스크립트가 기본적으로 지연적으로 동작하지 않기 때문에 추가
- array를 포함해 iterable이라면 모두 동작하도록 작성
L.flatMap = curry(pipe(L.map, L.flatten));
const flatMap = curry(pipe(L.map, flatten));
const it = L.flatMap(map(a => a * a), [[1, 2], [3, 4], [5, 6, 7], 8, 9]);
log(it.next()); // {value: 1, done: false}
log(it.next()); // {value: 4, done: false}
log(it.next()); // {value: 9, done: false}
log(it.next()); // {value: 16, done: false}
log(it.next()); // {value: 25, done: false}
flatMap의 활용
log(flatMap(L.range, [1, 2, 3])); // [0, 0, 1, 0, 1, 2]
2차원 배열 다루기
flatten을 이용해 2차원 배열을 다뤄보자
const arr = [
[1, 2],
[3, 4, 5],
[6, 7, 8],
[9, 10]
];
go(arr, flatten, log); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
go(arr, flatten, filter(a => a % 2), log); // [1, 3, 5, 7, 9]
go(arr, L.flatten, L.filter(a => a % 2), take(3), log); // [1, 3, 5]
지연성 / iterable 중심 프로그래밍 실무적인 코드
데이터를 실무에서 사용하는 구조로 바꾸어 이해해보자
const users = [
{
name: 'a', age: 21, family: [
{name: 'a1', age: 53}, {name: 'a2', age: 47},
{name: 'a3', age: 16}, {name: 'a4', age: 15}
]
},
{
name: 'b', age: 24, family: [
{name: 'b1', age: 58}, {name: 'b2', age: 51},
{name: 'b3', age: 19}, {name: 'b4', age: 22}
]
},
{
name: 'c', age: 31, family: [
{name: 'c1', age: 64}, {name: 'c2', age: 62}
]
},
{
name: 'd', age: 20, family: [
{name: 'd1', age: 42}, {name: 'd2', age: 42},
{name: 'd3', age: 11}, {name: 'd4', age: 7}
]
}
];
- 위는 4명의 사람과 그 가족에 대한 데이터
go(users, // users 정보로부터
L.map(u => u.family), // 각 user의 가족들 리스트를 모아
L.flatten, // 이를 flatten하여 한 배열에 객체를 모은다.
L.filter(u => u.age < 20), // 20세 미만의 미성년자만
L.map(u => u.name), // 이름만을 뽑는데,
take(4), // 4명만 뽑는다.
log // ["a3", "a4", "b3", "d3"]
)
- 객체 지향은 데이터 중심인 반면, 함수형 프로그래밍은 이미 작성된 함수들에 데이터를 맞추는 방식
- 함수가 데이터보다 우선순위가 높다.
댓글남기기