#1 결과를 만드는 함수 reduce, take
reduce를 통해 구현하는 함수
먼저, 객체로부터 url의 queryString을 만들어내는 함수를 만들어 볼 것이다.
const queryStr = obj => obj;
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// {limit: 10, offset: 10, type: "notice"}
이제 key와 value를 추출하는 코드를 작성해 볼 것이다.
const queryStr = obj => go(
obj,
Object.entries, // key, value를 entries로 변환
map(([k, v]) => `${k}=${v}`), // 구조분해를 통해 key와 value를 받음
reduce((a, b) => `${a}&${b}`) // reduce를 통해 세퍼레이터를 &로 줌
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice
queryStr은 obj를 받아서 그대로 obj로 전달하기때문에 아래처럼 pipe로 대체가 가능하다.
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
reduce((a, b) => `${a}&${b}`)
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice
이어서 reduce를 통해 join 함수를 만들어 볼 것이다. 이 조인 함수는 이터러블 값을 다 순회하면서 축약이 가능하기 때문에 array에 있는 join함수 보다 훨씬 다형성이 높다.
const join = curry((sep = ',', iter) => // join함수에서 기본값 ','
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
Object.entries,
map(([k, v]) => `${k}=${v}`),
join('&')
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice
다시말해서 위에서 선언한 join 함수는 배열이 아닌 경우에도 join 함수를 사용 할 수 있기 때문에 다형성이 높다.
아래 코드를 보자.
function* a(){
yield 10;
yield 11;
yield 12;
yield 13;
}
console.log(a().join(' - ')); // 결과를 만들 수 없다.
console.log(join(' - ', a())); // 10 - 11 - 12 - 13
위와 같은 제너레이터함수가 정의되어 있을 때, 일반적인 join 함수로는 결과를 만들 수 없지만, 위에서 선언한 join으로는 결과를 만들 수 있다.
그리고 만든 조인 함수는 reduce를 통해 축약을 했기 때문에, 이터러블 트로토콜을 따르고 있다는 말이여서, join에게 가기전에 만들어지는 값들을 지연할 수 있다. 그래서 map을 L.map이여도 동일한 결과를 나타낸다. 그리고 entries역시도 다음과 같이 선언하면 지연이 가능하다.
L.entries = function* (obj) {
for (const k in obj) yield [k, obj[k]];
};
const join = curry((sep = ',', iter) => // join함수에서 기본값 ','
reduce((a, b) => `${a}${sep}${b}`, iter));
const queryStr = pipe(
L.entries,
L.map(([k, v]) => `${k}=${v}`),
join('&')
);
console.log(queryStr({ limit : 10, offset: 10, type: 'notice' }));
// limit=10&offset=10&type=notice
take를 통해 구현하는 함수
이어서, take를 통해 find함수를 만들어 볼 것이다.
const users = [
{ age: 32 },
{ age: 31 },
{ age: 37 },
{ age: 28 },
{ age: 25 },
{ age: 32 },
{ age: 31 },
{ age: 37 }
];
const find = (f, iter) => go(
iter,
filter(f),
take(1), //하나의 값만 꺼내주는 역할
([a]) => a //배열을 꺼내서 주는 역할
);
console.log(find(u => u.age < 30, users));
// {age: 28}
위에서 작성한 find 함수를 보면, 한가지 아쉬운 점이 있다.
/* 비효율적인 코드 */
const find = (f, iter) => go(
iter,
filter(a=> (console.log(a), f(a))),
take(1),
([a]) => a
);
console.log(find(u => u.age < 30, users));
/*
{age: 32}
{age: 31}
{age: 37}
{age: 28}
{age: 25}
{age: 32}
{age: 31}
{age: 37}
{age: 28}
*/
filter에 콘솔을 찍어보면, take 하기 전에 모든 값들을 다 조회 함을 알 수 있다. 이는 매우 비효율적이므로 filter앞에 L을 붙여서 해당하는 값을 찾는 때가 딱 되면 조회를 멈춰서, 효율적으로 값을 찾을 수 있다.
/* 효율적인 코드 */
const find = (f, iter) => go(
iter,
L.filter(a=> (console.log(a), f(a))),
take(1),
([a]) => a
);
console.log(find(u => u.age < 30, users));
/*
{age: 32}
{age: 31}
{age: 37}
{age: 28}
{age: 28}
*/
아래 코드는 위 코드를 curry로 감싸서 깔끔하게 정리한 코드다.
const find = curry((f, iter) => go(
iter,
filter(a=> (console.log(a), f(a))),
take(1),
([a]) => a));
console.log(find(u => u.age < 30)(users));
다음과 같이도 나타낼 수 있다.
go(users,
L.map(u => u.age),
find(n => n < 30),
console.log);
// 28
#2 지연성 / 이터러블 중심의 코드 다루기
L.map, L.filter로 map과 filter 만들기
앞에서 배웠던 map, filter를 L.map, L.filter 와 take 조합으로 만들어보자.
먼저, take를 통해 map을 만드는 과정을 보자
const map = curry((f, iter) => go(
iter,
L.map(f),
take(Infinity)
));
//↓↓↓
const map = curry((f, iter) => go(
L.map(f, iter),
take(Infinity)
));
//↓↓↓
const map = curry(pipe(L.map, take(Infinity)));
filter도 같은방법으로 작성해서 아래와 같이 전부 정리했다.
const takeAll = take(Infinity);
/* L.map + take 로 map 만들기 */
const map = curry(pipe(L.map, takeAll));
console.log(map(a => a + 10, L.range(4)));
// [10, 11, 12, 13]
/* L.filter + take 로 filter 만들기 */
const filter = curry(pipe(L.filter, takeAll));
console.log(filter(a => a % 2, L.range(4)));
// [1, 3]
/* 앞에서 선언했던 L.map과 L.filter */
L.map = curry(function* (f, iter) {
for (const a of iter) {
yield f(a);
}
});
L.filter = curry(function* (f, iter) {
for (const a of iter) {
if (f(a)) yield a;
}
});
L.flatten, flatten
flatten는 하나의 배열 안에 있는 묶인 배열을 펼쳐서 하나의 배열로 만드는 역할을 해준다.
아래와 같이 동작을 한다.
console.log([...[1, 2], 3, 4, ...[5, 6], ...[7, 8, 9]]);
//[1, 2, 3, 4, 5, 6, 7, 8, 9]
이 함수를 구현해보자.
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;
}
};
var it = L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]);
console.log([...it]);
//[1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(take(3, L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]])));
// [1, 2, 3]
L.flatten 뿐만 아니라 take의 조합으로 즉시평가할수 있는 flatten도 만들 수 있다.
const flatten = pipe(L.flatten, takeAll);
console.log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]));
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
yield *를 활용하면 다음과 같이 코드를 변경 할 수 있습니다.
/* yield 사용 X */
L.flatten = function* (iter) {
for(const a of iter) {
if(isIterable(a)) for(const b of a) yield b
else yield a;
}
};
/* yield* 사용 O */
L.flatten = function* (iter) {
for(const a of iter) {
if(isIterable(a)) yield* a;
else yield a;
}
};
//"yield *iterable"은 "for (const val of iterable) yield val;" 과 같습니다.
만약 깊은 이터러블을 펼치고 싶다면 L.deepFlat도 구현 가능합니다.
L.deepFlat = function* f(iter) {
for (const a of iter) {
if (isIterable(a)) yield* f(a);
else yield a;
}
};
console.log([...L.deepFlat([1, [2, [3, 4], [[5]]]])]);
// [1, 2, 3, 4, 5];
L.flatMap, flatMap
flatMap는 map과 flatten을 동시에 하는 함수이다. flatMap 있는 이유는 자바스크립트가 기본적으로 지연적으로 동작하지 않기 때문이다.
console.log([[1, 2], [3, 4], [5, 6, 7]].flatMap(a => a.map(a => a * a)));
// [1, 4, 9, 16, 25, 36, 49]
console.log(flatten([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a * a))));
// [1, 4, 9, 16, 25, 36, 49]
flatMap은 map한 값에 flatten한 것과 동일한 값을 가짐을 볼 수 있다. 그럼 flatMap은 왜 있을까?? map과 flatten을 하는것은 매우 비효율적이기 때문이다. 아래 코드를 보면 왜 비효율인지 알 수 있다.
/* flatten 동작 전 map만 했을 경우 */
console.log([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a * a)));
// [Array(2), Array(2), Array(3)]
위에서 바로 flatMap하면 바로 원하는 해당 값이 나오지만, map을 한 후 flatten을 하면 배열을 만든 후에 flatten을 하기 때문에 약간의 비효율이 생긴다.
좀 더 다형성이 높은 L.flatMap을 만들어 보자.
L.flatMap = curry(pipe(L.map, L.flatten));
var it1 = L.flatMap(map(a => a * a), [[1, 2], [3, 4], [5, 6, 7]]);
console.log([...it1]); // [1, 4, 9, 16, 25, 36, 49]
var it2 = L.flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]);
console.log([...it2]); // [1, 2, 3, 4, 5, 6, 7]
L.flatMap이 있으면 역시 이를 활용해 즉시평가하는 flatMap을 또 만들 수 있다.
const flatMap = curry(pipe(L.map, flatten));
console.log(flatMap(a => a, [[1, 2], [3, 4], [5, 6, 7]]));
// [1, 2, 3, 4, 5, 6, 7]
2차원 배열 다루기
위에서 배운 코드를 기반으로 이제 다음과 같이 2차원 배열을 편히 다룰 수 있다.
const arr = [
[1, 2],
[3, 4, 5],
[6, 7, 8],
[9, 10]
];
go(arr,
L.flatten,
L.filter(a => a % 2),
L.map(a => a * a),
take(4),
reduce(add),
console.log);
//84
지연성 / 이터러블 중심 프로그래밍 실무적인 코드
한번 더 나아가 이번엔 실무적인 코드를 활용한 예제로 다시알아보자.
다음과 같은 사용자코드가 있다고 보자.
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}
]
}
];
아래와 같이 2차원배열에서 다뤘던 방법처럼 실무에서도 똑같이 적용이 가능하다.
go(users,
L.map(u => u.family),
L.flatten,
L.filter(u => u.age < 20), //아직 성인이 아닌 가족을 뽑음
L.map(u => u.age),
take(4), // 16, 15, 19, 11
reduce(add),
console.log); // 61
'Web[웹] > ES6+ 함수형 언어' 카테고리의 다른 글
[JS: ES6+] 비동기: 동시성 프로그래밍 (2) (0) | 2019.10.21 |
---|---|
[JS: ES6+] 비동기: 동시성 프로그래밍 (1) (0) | 2019.10.17 |
[JS: ES6+] 제너레이터/이터레이터 프로토콜로 구현하는 지연 평가 (1) (0) | 2019.10.08 |
[JS: ES6+] 장바구니 예제로 코드를 줄이고 HTML로 표현해보기 (0) | 2019.10.07 |
[JS: ES6+] 코드를 값으로 다루어 표현력 높이기 (go, pipe) (0) | 2019.10.04 |