*아래에서 사용될 함수(go, curry...)들은 이전 포스트들에서 설명되어 있습니다.
📗함수형 프로그래밍에서 reduce 와 take의 역할
어떠한 데이터를 최종적으로 만들어 낼 것인가? 에서 결과를 만들어주는 reduce와 take 계열의 함수들은 중요하다.
앞에서는 지연평가를 이용하여 함수를 살펴보았고, 실제로 평가가 미루어져서 마지막 take에서 부터 연산이 실행함을 알 수 있었다.
그렇다면 다시 말해, reduce와 take계열의 함수는 연산의 시작점 이라고 볼 수 있을 것이다.
queryStr 함수 생성으로 알아보는 함수형 프로그래밍적 사고
아래 코드 queryStr 함수 실행시의 주석을 보면은
L.entries = function* (obj) {
for (const k in obj) yield [k, obj[k]];
};
const join = curry((sep = ',', iter) => // 이 조인은 이터러블 프로토콜을 따르기 때문에 더욱 다형성이 높다.
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
L.entries, // Object 내에 모든 프로퍼티를 배열 형태로 받고
L.map(([k, v]) => `${k}=${v}`), // 받은 배열을 ?=? 형태로 만들어 준다음
join('&')); // 최종 스트링을 return 해 줘야겠다.
log(queryStr({limit: 10, offset: 10, type: 'notice'}));
사고의 흐름과 내는 결론이 매우 명확하게 나타난다. 여기서의 join은 reduce 계열의 함수로 어떠한 결과를 만들어낼지를 결정한다.
이러한 take, reduce 계열의 함수들이 연산의 시작점이며, 최종 결과 산출물을 결정한다는 점을 고려하자.
📗map, filter 리팩토링
그렇다면 L.map 과 L.filter 가 있는 지금 시점에서 리팩토링을 해보자
L.map 과 map의 다른 점은 평가시점이기 때문에 L.map의 결과를 바로 평가한 값을 리턴하면 그것이 map과 동일하게 동작하는 함수임을 알 수 있다.
L.map = curry(function* (f, iter) {
for (const a of iter) {
yield f(a);
}
});
// iter를 L.map으로 순회하며
// take로 전부 평가한 값을 리턴하는 함수
const map = curry(pipe(L.map, take(Infinity)));
위 코드처럼 깔끔히 정리된다. filter도 마찬가지로
L.filter = curry(function* (f, iter) {
for (const a of iter) {
if (f(a)) yield a;
}
});
// iter를 L.filter로 순회하며
// take로 전부 평가한 값을 리턴하는 함수
const filter = curry(pipe(L.filter, take(Infinity)));
위와 같이 정리된다.
📗L.flatten , L.flatMap
2차원 배열을 1차원 배열로 변환하는 L.flatten도 만들어보자.
L.flatten = function *(iter) {
for (const a of iter) {
if (isIterable(a)) for (const b of a) yield b
else yield a;
}
};
// yield *iterable`은 `for (const val of iterable) yield val; 임을 이용항 아래와같이 만들 수 있다.
L.flatten = function *(iter) {
for (const a of iter) {
if (isIterable(a)) yield *a;
else yield a;
}
};
재귀를 사용하면 배열이 어떤 depth를 가지든 전부 1차원 배열로 만들 수 있는 L.deepFlat 함수도 만들어보면 아래와 같다.
L.deepFlat = function* f(iter) {
for (const a of iter) {
if (isIterable(a)) yield* f(a);
else yield a;
}
};
위 코드들을 기반으로 L.flatMap 함수를 만들 수 있는데, 기존 flatMap 함수는 지연적으로 작동하지 않기 때문에 지연적으로 작동하는 L.flatMap 를 만들어보자.
// 지연적으로 map 돌리고
// 지연적으로 평탄화 하는 함수
L.flatMap = curry(pipe(L.map, L.flatten));
눈치 챘을지 모르겠지만 만드는 게 매우 간단하다. 아래와 같이 사용해보면
var it = L.flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]);
log(it.next()); // {value: 1, done: false}
log(it.next()); // {value: 2, done: false}
log(it.next()); // {value: 3, done: false}
log(it.next()); // {value: 4, done: false}
log(it.next()); // {value: 5, done: false}
log(it.next()); // {value: 6, done: false}
마치 2차원 배열이 아닌 것처럼 잘 작동하며, 지연평가도 문제 없이 적용된다.
📗실무적인 코드로 감잡기
지금까지의 함수들로 실제 있을법한 데이터를 사용해서 함수형 프로그래밍으로 로직을 완성해보자
var 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}
] }
];
go(users,
L.flatMap(u => u.family), // 2차원 배열을 평탄화하는 동시에 family만 꺼내자
L.filter(u => u.age > 20), // 꺼낸 family중 21살 이상만 남기자
L.map(u => u.age), // 남긴 family 중 날짜만 가져오자
take(4), // 그중 4명만 뽑자
reduce(add), // 전부 나이를 더하자.
log);
주석들을 보면 알겠지만, 사고의 흐름과 코드가 일치하고 실무와 매우 잘 맞아 떨어진다. 또한 함수들은 지연 평가이기 때문에, 데이터의 크기가 크더라도 비슷한 효율을 낼 것이다.
이렇게 사고하는 프로그래밍 LISP 이라고 하며 리스트 프로세싱이라고도 부른다.
'함수형 프로그래밍과 ES6+' 카테고리의 다른 글
[함수형 프로그래밍과 ES6+] 동시성 - 1 (2) | 2024.06.08 |
---|---|
[함수형 프로그래밍과 ES6+] 지연성 - 1 (0) | 2024.05.25 |
[함수형 프로그래밍과 ES6+] 코드를 값으로 바꾸어 표현력 높이기 (0) | 2024.05.21 |
[함수형 프로그래밍과 ES6+] 함수형 프로그래밍을 위한 기본 지식 (0) | 2024.05.19 |