해당 강의를 듣고 정리한 글입니다.... 🐿️
https://www.inflearn.com/course/functional-es6
query string을 만드는 함수를 만들어보자.
const queryStr = (obj) =>
go(
obj,
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
go 대신 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
array의 join 함수는 array 프로토타입에만 있는 함수이다. reduce는 이터러블 객체를 순회하면서 축약을 할 수 있기 때문에 더 다형성이 높다. 그래서 reduce를 통해 join 함수를 만들어보자.
const join = curry((sep = ',', iter) => 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 함수는 조합성, 다형성이 높다. 또한 map 대신에 저번에 만들었던 L.map을 사용해 지연평가도 가능하다.
Object.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' });
reduce, take 함수는 결과값을 만들어 내는 함수이다. join은 reduce를 통해 결과값을 만들어냈다면, 이번에는 take 함수를 통해 결과값을 만들어내는 find 함수를 만들어보자.
find는 조건에 해당되는 특정 값을 하나만을 가져오는 함수이다. L.filter와 take을 사용하면서 전체배열 순회 -> filter -> take로 하나의 배열을 가져오는 방식이 아니라 배열의 원소를 하나씩 꺼내오고 -> filter -> take 여기서, 값 1개를 찾는다면 더이상 배열을 순회하지 않는다. => 훨씬 효율적이다!
const find = (f, iter) => go(iter, L.filter(f), take(1), console.log); // [ { age: 25 } ]
find((u) => u.age < 30, users);
L.map,L.filter를 통해서 map,filter 함수를 만들어보았다.
L.filter = curry(function* (f, iter) {
for (const a of iter) if (f(a)) yield a;
});
L.map = curry(function* (f, iter) {
for (const a of iter) yield f(a);
});
const map = curry(pipe(L.map, take(Infinity), console.log));
const filter = curry((f, iter) => go(L.filter(f, iter), take(Infinity), console.log));
map((a) => a + 10, L.range(4)); // [ 10, 11, 12, 13 ]
filter((a) => a % 2, L.range(4)); // [ 1, 3 ]
take(Infinity)가 중복되니까 takeAll로 함수를 추출하면, 이렇게 작성할 수 있다.
const takeAll = take(Infinity);
L.filter = curry(function* (f, iter) {
for (const a of iter) if (f(a)) yield a;
});
L.map = curry(function* (f, iter) {
for (const a of iter) yield f(a);
});
const map = curry(pipe(L.map, takeAll, console.log));
const filter = curry((f, iter) => go(L.filter(f, iter), takeAll, console.log));
map((a) => a + 10, L.range(4)); // [ 10, 11, 12, 13 ]
filter((a) => a % 2, L.range(4)); // [ 1, 3 ]
flatten 함수를 만들어볼건데, flatten 함수는 아래의 코드처럼 다 펼쳐서 하나의 배열로 만드는 함수이다.
console.log([...[1, 2], 3, 4, ...[5, 6], ...[7, 8, 9]]); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
일차원 배열로 변형해주고, iter.next()로 원하는 만큼 값을 반환해주는 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;
}
};
const it = L.flatten([1, 2], 3, 4, [5, 6], [7, 8, 9]);
console.log(it.next()); // 1
console.log(it.next()); // 2
console.log(it.next()); // 3
console.log(it.next()); // 4
즉시 평가하는 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]
또 이렇게 작성하면 원하는 3개의 값만 가져올 수 있다.
console.log(take(3, L.flatten([[1, 2], 3, 4, [5, 6], [7, 8, 9]]))); // [1,2,3]
flatMap은 flatten과 map을 동시에 하는 함수이다.
console.log([[1,2],[3,4],[5,6,7],8,9,[10]].flatMap(a => a)); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
이렇게만 사용하면 flatten과 동일한데, flatMap은 아래처럼 하는것도 가능하다.
[[1, 2], [3, 4], [5, 6, 7]].flatMap((a) => a.map((a) => a * a)) // [1, 4, 9, 16, 25, 36, 49]
map을 한 후에 flatten을 한 것과 flatMap은 동일하게 동작한다.
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과 flatten + map은 시간복잡도가 동일하기 때문에 엄청난 효율이 생겼다고는 할 수 없다. 그러나 앞에 있는 3개만 필요하다거나 하면 좀 더 효율적이고, array로만 동작하는 것이 아니기 때문에 좀 더 다형성이 있다.
L.flatMap = curry(pipe(L.map, L.flatten));
const it = L.flatMap(
map((a) => a * a),
[[1, 2], [3, 4], [5, 6, 7]]
);
console.log([...it]); // [1, 4, 9, 16, 25, 36, 49]
즉시평가하는 flatMap 함수도 만들 수 있다.
const flatMap = curry(pipe(L.map, flatten));
const arr = [
[1, 2],
[3, 4, 5],
[6, 7, 8],
];
go(
arr,
L.flatten,
L.filter((a) => a % 2),
take(3),
console.log
); // [1, 3, 5]
이렇게 작성하면 지연적으로 동작하기 때문에 필요한 값까지만 순회한다. 여기에 reduce를 사용할 수 도 있고, 다양하게 조합이 가능하다.
우리가 공부했던 내용들이 왜 필요하고, 실무에서는 어떻게 사용할까?
이차원 배열과 마찬가지로 users의 데이터를 함수의 조합으로 상황에 맞게 가져온다. 이런식으로 실무에서 꽤 많이 쓰인다고 한다.!
const 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),
L.filter(u => u.age > 20),
L.map(u => u.age),
take(4),
reduce(add),
console.log);
객체지향은 데이터를 우선적으로 준비하고 메서드들을 나중에 작성해 나간다면, 함수형 프로그래밍은 이미 만들어져 있는 함수들의 조합으로 데이터를 구성한다.