결과를 만드는 함수 reduce, take
map이나 filter함수가 배열이나 이터러블한 모나딕한 값에 내부에 있는 원소들에게 함수들을 합성해놓는 역할을 한다면, reduce나 take는 최종적으로 결과를 만들어내는 함수입니다.
map이나 filter같은 함수들은 지연성을 가질 수 있고, reduce나 take는 결과를 만드는 시작점을 만들기 때문에 iterator의 값을 꺼내어 연산을 시작합니다. take함수는 지연성을 가질 수도 있지만, 몇개로 떨어질지 모르는 배열에서 특정 갯수만큼 배열로 축약 및완성하는 특성이 있기 때문에 지연성을 가지기보단 함수가 호출된 시점에 연산이 이루어지는 것이 더 낫습니다.
queryStr 함수 만들기
결과를 만드는 함수 reduce를 응용해서 객체로부터 url의 queryString을 만드는 함수 queryStr을 만들어봅니다.
const queryStr = pipe(
Object.entries,
map(([k,v])=>`${k}=${v}`),
reduce((a,b)=>`${a}&${b}`)
);
log(queryStr({limit: 10, offset:10, type: 'notice'})); // limit=10&offset=10&type=notice
JavaScript
복사
Array.prototype.join보다 다형성이 높은 join함수
기존 Array에서 제공하는 join 함수는 지연평가를 제공하지않습니다. 그렇기 때문에 지연성도 제공하여 더 다형성이 높은 join함수를 만들어 봅시다.
const join = curry((sep =",", iter) => reduce((a, b)=> `${a}${sep}${b}`, iter));
JavaScript
복사
•
기존 Array.prototype.join 사용
⇒ 즉시평가 가능한 배열만 가능. Iterator는 불가
[1,2,3,4].join('-') //1 - 2 - 3 - 4
JavaScript
복사
•
새로만든 join 사용
function *a(){
yield 10;
yield 11;
yield 12;
yield 13;
}
join(' - ', a());//10 - 11 - 12 - 13
JavaScript
복사
•
느긋한 L.map을 사용할 때 join사용하기
const queryStr = pipe(
Object.entries,
L.map(([k, v]) => `${k}=${v}`),
function (a) {
log(a);
return a;
},
join('&')
);
//실행결과
//Generator {<suspended>}
//board.html?_ijt=5a6paisfamop82mebiurepmg4p:33 limit=10&offset=10&type=notice
JavaScript
복사
•
join으로 넘어갈 당시에 인자값은 iterable해서 실제 값이 생성되있진 않은 지연성이 있습니다.
•
L.map역시 이터레이터를 처리할 수 있어서 Object.entries가 아닌 지연평가 L.entries를 만들어서 내려줘도 됩니다.
L.entries = function* (obj) {
for (const key in obj) {
yield [key, obj[key]];
}
};
const queryStr = pipe(
L.entries,
function (a) {
log(a);
return a;
},
L.map(([k, v]) => `${k}=${v}`),
join('&')
);
log(queryStr({limit: 10, offset:10, type: 'notice'}));
//실행결과
//L.entries {<suspended>}
//board.html?_ijt=5a6paisfamop82mebiurepmg4p:33 limit=10&offset=10&type=notice
JavaScript
복사
take, find
앞서 만든 join 함수는 reduce 계열의 함수라 할 수 있습니다. reduce함수를 이용해서 만들었기 때문입니다.
그리고 entries를 통해서 만드는 함수는 또 map을 통해서 만드는 함수라 칭할 수 있는데 이렇게 함수를 사용해서 만드는 것을 함수 계열(계보)를 가지는 식으로 함수를 만들 수 있습니다. 이전에 만들어 본 queryStr이 reduce함수로 결론을 내는 함수였다면 find함수는 take로 결론을 만들어내는 함수입니다.
•
함수
const users =[
{ age: 32},
{ age: 31},
{ age: 37},
{ age: 28},
{ age: 25},
{ age: 32},
{ age: 31},
{ age: 37},
{ age: 19},
{ age: 22},
];
const find = curry((f, iter) =>go(
iter,
L.filter(f),
take(1),
([a])=>a
));
log(find(u => u.age < 30, users));//{age: 28}
JavaScript
복사
◦
인자값으로 받은 iter값을 L.filter로 이터러블객체를 take에 전달하면 take함수에서 실제로 결과를 만들어내기위해 값들을 호출하게되고 filter condition에 부합하는 값을 찾으면 반환하며 전달후 구조분해하여 반환합니다.
◦
지연된 평가를 사용하였기에 filter 부분에서 모든 값을 다 condition과 비교하며 비교후 완성된 값을 반환하는게 아닙니다. generator 호출을 계속 시도하여 filter condition과 비교하며 부합된 값을 찾는데 take(1)이기에 하나의 값을 찾는순간 로직은 종료되며, 만일, users의 목록이 1000만개인데 10만개쯤에서 결과를 찾는다면 그 뒤 990만번의 불필요한 연산을 막을 수 있습니다.
L.map, L.filter로 map과 filter 만들기
지연평가 L.map과 L.filter로 즉시평가 map과 filter를 만드는 방법은 결과를 만들어내는 함수 take를 이용하면 간단하게 구현이 가능합니다.
const mapV2 = curry(pipe(L.map, take(Infinity)));
const filterV2 = curry(pipe(L.filter, take(Infinity)));
JavaScript
복사
•
take를 통해 지연성을 제공하는 이터러블객체를 실제 호출하여 값이 더이상 안나올때까지 꺼내게 됩니다.
L.flatten, flatten
이터레이터 객체(배열,맵,등..)에는 내부에 또 다른 이터레이터를 가지고 있을 수 있습니다. flatten은 이런 이터레이터 안의 이터레이터를 평탄화해서 펼쳐주는 역할을 합니다.
•
L.flatten 함수
const isIterable = a => a && a[Symbol.iterator];
L.flatten = function* (iter) {
for (const a of iter) {
if (isIterable(a)) {
for (const b of a) yield b;
} else {
yield a;
}
}
};
JavaScript
복사
◦
isIterable을 통해 이터러블객체인지 평가합니다.
•
즉시평가 flatten
const flatten = pipe(L.flatten, takeAll);
JavaScript
복사
◦
이전에 만든 지연성 함수를 결과를 도출해내는 함수(take, reduce)를 통해 즉시평가가 가능하게 합니다.
yield *, L.deepFlat
yield* 은 for(const val of iterable) yield val과 동일합니다.
L.flatten = function* (iter) {
for (const a of iter) {
if (isIterable(a)) {
//before
for (const b of a) yield b; //yield *iterable로 변경 가능
//after
yield *a;
} else {
yield a;
}
}
};
//after
JavaScript
복사
기존 flatten은 이터레이터안의 이터레이터는 펼쳐주지만 거기서 더 depth를 진행해 더 깊은 이터레이터의 경우 펼쳐주지 못합니다. 그런경우 아래와 같이 deepFlat을 구현해 사용합니다.
L.deepFlat = function* f(iter) {
for (const a of iter) {
if (isIterable(a)) {
yield *f(a);
} else {
yield a;
}
}
};
log([...L.deepFlat([[1,2], 3,4,[5,6,7], [8,[9,10]]])]);//[1,2,3,4,5,6,7,8,9,10]
JavaScript
복사
•
deepFlat은 function* f(iter){...}으로 기명함수 표현식을 사용해서 재귀함수를 구현했습니다.
기명 함수 표현식(Named Function Expression, FNE)
이름이 있는 함수 표현식을 나타내는 용어로 해당 기능을 사용할 경우 두 가지 변화가 생깁니다.
1. 이름을 사용해서 함수 표현식 내부에서 자기자신을 참조할 수 있습니다.
2. 기명 함수 표현식 외부에서는 그 이름을 사용할 수 없습니다.
위와 같은 변화를 deepFlat에 적용시켜보면 해당 함수는 f 라는 기명함수 표현식을 사용했습니다.
그렇기에 외부에서 f(iter)를 호출한다고 해당 deepFlat을 사용할수는 없습니다.(2번항목 참고)
하지만 내부에서는 자기자신을 호출할 때 사용 할 수 있기 때문에 isIterable(a)를 통해 순회하고 있는 이터러블 내의 값 a가 또 다른 이터레이터일 경우 자기자신을 재귀적으로 호출해 flatten작업을 수행해서 아무리 깊은 depth의 이터레이터도 다 펼쳐놓을 수 있게 됩니다.
L.flatMap, flatMap
flatMap은 flatten과 map을 동시에 작업해주는 함수인데, 최신 자바스크립트에서는 해당 기능이 추가되어있습니다.
해당 함수의 존재이유는, 자바스크립트가 기본적으로 지연평가가 아닌 즉시평가이기 때문입니다.
•
Javascript에서 제공하는 flatMap
arr.flatMap(callback(currentValue[, index[, array]])[, thisArg])
JavaScript
복사
◦
callback: 새로운 배열의 엘리먼트를 생성하는 함수
1.
currentValue: 배열에서 처리되는 현재 엘리먼트,
2.
index(Optional): 배열에서 처리되고 있는 현재 엘리먼트 인덱스
3.
array(Optional): map이 호출된 배열
◦
thisArg(Optional): callback실행해서this로 사용할 값
let arr1 = [1, 2, 3, 4];
arr1.flatMap(x => [x * 2]);// [2, 4, 6, 8]
JavaScript
복사
flatMap은 결국 map과 flatten로직의 결합이기 때문에 아래와같이 사용해도 동일합니다.
flatten([[1,2],[3,4],[5,6,7]].map(a=>a.map(a=>a*a))); //[1, 4, 9, 16, 25, 36, 49]
JavaScript
복사
하지만 즉시평가 flatMap의 경우 불필요한 연산을 해야하는 경우가 있기 때문에 지연성을 제공하는 flatMap을 만들 수 있습니다.
L.flatMap = curry(pipe(L.map, L.flatten));
//즉시평가 flatMap
const flatMap = curry(pipe(L.flatMap, takeAll));
JavaScript
복사
2차원 배열 다루기
2차원 배열을 가지고 기존에 구현한 함수들을 응용해 로직을 수행할 수 있습니다.
•
2차원 배열 원천데이터 예제
const arr = [
[1, 2],
[3, 4, 5],
[6, 7, 8],
[9, 10]
];
JavaScript
복사
1. arr에서 홀수만 꺼내기
go(arr,
L.flatten,
L.filter(a => a % 2),
takeAll,
log
) // [1, 3, 5, 7, 9]
JavaScript
복사
2. arr에서 홀수만 꺼내 모두 더하기
go(arr,
L.flatten,
L.filter(a => a % 2),
reduce(add),
log
)//25
JavaScript
복사
3. arr에서 홀수만 3개만 꺼내 제곱하기
go(arr,
L.flatten,
L.filter(a => a % 2),
take(3),
map(a=>a*a),
log
)//[1, 9, 25]
JavaScript
복사
참고 - 지연평가 함수(L.func)의 성능 향상
최신 javascript에서도 flatMap, map foreach, reduce등 대부분의 기능을 기본적으로 제공하고 있습니다. 그런데 어째서 이렇게 사용자정의로 구현한 함수들을 사용할까요?
위의 코드들이 기본으로 제공되는 API 로직과 가장 큰 차이점은 지연평가로인한 불필요한 연산및 결과도출의 생략입니다.
즉시평가로 할 경우 filter등에서 arr의 모든 값 1~10까지 다 꺼내어 condition function과 비교후 맞는 값들을 꺼냅니다. 하지만, 지연평가의 경우 필요한 수치까지 다 가져오면(ex:3개만 가져오는 take(3) 함수) 더이상의 결과도출을 하지 않고 로직이 종료됩니다.
이밖에도 기존에 만든 함수들을 통해 2차원 배열을 통한 로직수행도 충분히 가능할 뿐 아니라 성능향상까지 가능합니다.
이터러블 중심 프로그래밍 실무적인 코드
기존에 간단한 숫자만 가지고 함수작성및 예제를 구현해봤기에, 감을 잡기 어려울 수 있고, 실무와의 접근성이 낮아보이기에 실무데이터와 비슷한 데이터를 통해 로직을 구현해봅시다.
•
예제 데이터
let 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}
]
}
];
JavaScript
복사
•
모든 유저의 가족들 중 미성년자를 3명만 찾아서 반환하라
go(users,
L.map(u=>u.family), users의 obj별 family 의 value를 꺼내어 반환
L.flatten, // 2차원 배열의 평탄화
L.filter(a => a.age < 20), // 20세 미만 유저만 필터
take(3), // 3명까지만 결과 도출
log
);
//실행결과
0: {name: "a3", age: 16}
1: {name: "a4", age: 15}
2: {name: "b3", age: 19}
JavaScript
복사