이전 포스팅에서의 reduce, take함수는 iterator에서 값을 하나씩 꺼내 결과를 만들어내는 함수로 생각하고 코드를 작성하면 유용하다. 그리고 그런 함수들을 이용한다면 조금 더 다형성이 높은 코드를 작성할 수 있다.
reduce함수를 활용한다면 array일때만 가능했던 기존의 join보다 다형성이 높은 join함수를 만들 수 있다. 그리고 iterable 프로토콜을 따르기 때문에 앞에서 배웠던 지연평가도 적용이 가능하다.
const join = curry((seq = ',', iter) => reduce((a, b) => `${a}${seq}${b}`, iter));
function *genfunc() {
yield '010';
yield '1234';
yield '5678';
}
console.log(genfunc().join('-'));
// 제네릭 객체는 join함수를 사용할 수 없었다.
// TypeError: genfunc(...).join is not a function
console.log(join('-', genfunc()));
// 010-1234-5678
take함수를 활용하여 find함수를 만든다면 다음과 같이 만들 수 있다.
const users =[
{age: 32,},
{age: 31,},
{age: 37,},
{age: 29,},
{age: 15,},
{age: 40,},
{age: 10,},
{age: 50,},
{age: 60,},
]
const find = (f, iter) => go(
iter,
L.filter(f),
take(1) // 하나의 요소만을 꺼내는 함수
);
// 30세 미만인 유저를 찾는 find함수
console.log(find(u => u.age < 30, users));
// [ { age: 29 } ]
하지만 위에서 작성한 find함수는 한가지 아쉬운점이 있다.
해당 코드의 실행 순서를 따라가보면 결과값으로 하나의 요소만을 필요로 하지만 take하기 전에 모든 요소들을 다 조회하는 비효율적인 방식으로 동작하고 있는 것을 볼 수 있다. 그래서 이전에 작성했던 지연평가가 적용된 L.filter함수를 활용하여 원하는 시점에 평가함으로서 불필요한 연산을 줄일 수 있는 find함수로 바꾸었다.
const find = (f, iter) => go(
iter,
L.filter(f), // 지연평가
take(1)
);
console.log(find(u => u.age < 30, users));
// [ { age: 29 } ]
앞서 작성했던 map, filter를 L.map, L.filter 와 take 조합으로 조금 더 간단하게 작성할 수 있다.
take함수의 역할은 iterator의 값을 하나씩 평가하면서 배열에 push하다가 제한된 갯수 도달하거나 더이상 평가할 값이 없을 때 해당 배열을 반환하는 함수이다. 그렇기 때문에 Infinity로 무한정 돌려도 더이상 평가할 값이 없을 때 반환된다.
const map = curry((f, iter) => {
let res = [];
for(const a of iter) {
res.push(f(a));
}
return res;
});
// 1단계
const map = curry((f, iter) => go(
iter,
L.map(f),
take(Infinity)
));
// 2단계
// L.map의 인자로 iterable을 바로 넘겨도 된다.
const map = curry((f, iter) => go(
L.map(f, iter),
take(Infinity)
));
// 3단계
// 인자가 곧 그 다음 시작하는 함수의 인자이기 때문에 pipe함수로 변경
const map = curry(pipe(L.map, take(Infinity)));
const filter = curry((f, iter) => {
let res = [];
for(const a of iter) {
if(f(a)) res.push(a);
}
return res;
});
// 1단계
const filter = curry((f, iter) => go(
iter,
L.filter(f),
take(Infinity)
));
// 2단계
const filter = curry((f, iter) => go(
L.filter(f, iter),
take(Infinity)
));
// 3단계
const filter = curry(pipe(L.filter, take(Infinity)));
flatten 함수는 모든 하위 배열 요소를 하나의 배열에 이어붙인 역할을 하는 함수이다.
[[1, 2], 3, 4, [5, 6], [7, 8, 9]] -> [1, 2, 3, 4, 5, 6, 7, 8, 9]
기존의 flat()와의 결과의 차이는 없지만 array뿐만 아니라 iterable한 것들도 활용할 수 있도록 유연한 flat()함수를 작성한다면 아래와 같이 작성할 수 있다.
const isIterable = (a) => a && a[Symbol.iterator];
L.flatten = function *f(iter) {
for(const a of iter) {
if(isIterable(a)) yield *a; // 해당 코드는 아래와 동일하게 동작한다.
// if(isIterable(a)) for (const b of a) yield b;
else yield a;
}
}
go(
[[1, 2], 3, 4, [5, 6, 7]],
L.flatten,
take(Infinity),
console.log
);
// [1, 2, 3, 4, 5, 6, 7]
또한 즉시평가도 가능한 flatten함수도 구현할 수 있다.
const flatten = pipe(L.flatten, take(Infinity));
console.log(flatten([[1, 2], 3, 4, [5, 6, 7]]));
// [1, 2, 3, 4, 5, 6, 7]
만약 depth가 깊은 이터러블을 펼치고 싶다면 L.deepFlat함수도 쉽게 구현할 수 있다.
L.deepFlat = function* f(iter) {
for (const a of iter) {
if (isIterable(a)) yield* f(a);
else yield a;
}
};
const arr3 = [1, 2, [3, 4, [5, 6]]];
go(
arr3,
L.flatten,
take(Infinity),
console.log
);
// [1, 2, 3, 4, 5, 6]
flatMap는 map과 flatten을 동시에 하는 함수로 기존에는 지연적으로 동작하지 않아 비효율적이기 때문에 사용하는 함수라고 한다.
const arr = [[1, 2], [3, 4], [5, 6, 7]];
console.log(arr.flatMap(a => a.map(a => a + 1)));
// [2, 3, 4, 5,6, 7, 8]
// 조금 더 효율성이 있는 flatMap을 구현
L.flatMap = curry(pipe(L.map, L.flatten));
go(
arr,
L.flatMap(map(a => a + 1)),
take(Infinity),
console.log
);
// [2, 3, 4, 5, 6, 7, 8]
L.flatMap이 있으면 마찬가지로 이를 활용하여 즉시평가하는 flatMap함수도 만들 수 있다.
const flatMap = curry(pipe(L.map, flatten));
console.log(flatMap(map(a => a + 1), arr));
// [2, 3, 4, 5, 6, 7, 8]
다음과 같이 회사 동료들의 가족구성원을 보여주는 데이터가 존재한다고 가정했을 때 지금까지 작성해왔던 함수의 조합으로 동료들의 가족구성원 중 성인이 아닌 사람들의 이름을 4번째까지 뽑는다면 다음과 같이 적용이 가능하다.
const colleagues = [
{
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}
]
}
];
go(colleagues,
L.flatMap(c => c.family),
L.filter(c => c.age < 20), // 성인이 아닌 동료 가족구성원들
L.map(c => c.name),
take(4),
console.log
);
// [ 'a3', 'a4', 'b3', 'd3' ]
지금까지는 데이터 중심적인 사고방식으로 코드를 작성해왔다면 데이터를 어떻게 구성할지를 먼저 만들어내고 코딩하는 것이 아닌 조합되어있는 함수에 맞게 데이터를 구성하는 방식으로 코딩을 한다고 생각하면 될 것 같다. 그리고 이것이 함수가 우선순위에 있는 프로그래밍 방식인 함수형 프로그래밍이라고 한다.