이 글은 유인동님의 함수형 프로그래밍 강의내용을 정리한 글입니다.
reduce
, take
객체로부터 url의 queryString을 만들어내는 함수를 만들어 보자.
const queryStr = obj =>
go(
obj,
Object.entries, // [[key, value], [key, value],...]를 변환
map(([k, v]) => `${k}=${v}`), // 구조분해를 통해 key와 value를 받음
reduce((a, b) => `${a}&${b}`) // '&'로 연결
);
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
함수를 만들어 보자. 이 join
함수는 이터러블 값을 순회하면서 축약하기 때문에 Array에서만 사용할 수 있는 join
메서드 보다 훨씬 다형성이 높다.
const join = curry((sep = ',', iter) =>
reduce((a, b) => `${a}${sep}${b}`, iter)
);
function* a() {
yield 10;
yield 11;
yield 12;
yield 13;
}
console.log(a().join(' - ')); // 에러!, join메서드는 Array에만 사용가능
console.log(join(' - ', a())); // 10 - 11 - 12 - 13
위와 같이 제너레이터함수가 정의되어 있을 때, 일반적인 join
메서드로는 결과를 만들 수 없지만, 위에서 선언한 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) =>
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
함수를 통해 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), // take 하기 전에 모든 값들을 다 조회한다.
take(1), // 하나의 값만 리턴한다.
([a]) => a // 구조분해, 배열에서 값을 꺼내 준다. take가 배열로 감싸서 리턴하기 때문
)
);
console.log(find(u => u.age < 30, users));
// {age: 28}
filter
함수에 콘솔을 찍어보면, take
함수 이전에 모든 값들을 다 조회하므로 비효율적이다.
L.filter
지연 평가const find = (f, iter) => go(
iter,
L.filter(f),
take(1),
([a]) => a
);
console.log(find(u => u.age < 30, users));
// {age: 28}
지연평가 함수인 L.filter
를 사용하면 필요할 때만 값을 조회하므로 효율적이다.
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));
// {age: 28}
앞에서 배웠던 map
, filter
함수를 L.map
, L.filter
와 take
조합으로 만들어보자.
L.map
+ take
로 map
만들기// L.map
L.map = curry(function* (f, iter) {
for (const a of iter) {
yield f(a);
}
});
/* L.map + 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)));
L.filter
+ take
로 filter
만들기// L.filter
L.filter = curry(function* (f, iter) {
for (const a of iter) {
if (f(a)) yield a;
}
});
/* L.filter + take 로 filter 만들기 */
const filter = curry(pipe(L.filter, take(Infinity)));
console.log(filter(a => a % 2, L.range(4)));
// [1, 3]
flatten
함수는 하나의 배열 안에 있는 묶인 배열을 펼쳐서 하나의 배열로 만드는 역할을 해준다.
[[1, 2], 3, 4, [5, 6], [7, 8, 9]]
→ [1, 2, 3, 4, 5, 6, 7, 8, 9]
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;
if (isIterable(a)) yield* a;
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]
flatten
take의 조합으로 즉시평가할수 있는 flatten도 만들 수 있다.
const flatten = pipe(L.flatten, take(Infinity));
console.log(flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]));
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
yield*
를 활용하면 다음과 같이 코드를 변경 할 수 있습니다.
yield *iterable
은for (const val of iterable) yield val;
과 같다.
/* 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;
}
};
만약 깊은 이터러블을 모두 평치고 싶다면 아래와 같이 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];
flatMap은 map한 값에 flatten한 것과 동일한 값을 가진다. 하지만 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]
// map + flatten
console.log(flatten([[1, 2], [3, 4], [5, 6, 7]].map(a => a.map(a => a * a))));
// [1, 4, 9, 16, 25, 36, 49]
먼저 map을 하고 그 결과를 flatten하면 flatMap과 정확히 일치한다. 하지만 두 코드는 배열의 처음부터 끝까지 순회하기 때문에 시간복잡도의 차이는 없다.
하지만 flatMap된 결과에서 take함수를 사용하여 일부분만 뽑아 올때는 지연성으로 작성되지 않았기 때문에 비효율적으로 동작하게된다.
따라서 지연성이 있는 L.flatmap을 구현해보는것이 이번 강의 목표다.
다형성이 높은 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.flatten이 아닌 flatten을 사용하면 즉시 평가하는 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]
L.flatMap을 활용하면 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}
]
}
];
go(users,
L.map(u => u.family),
L.flatten, // 2차원 배열 펼치기
L.filter(u => u.age < 20), // 20세 이하 필터
L.map(u => u.age), // 나이만
take(3), // 3개만
console.log); // 16, 15, 19