해당 강의를 듣고 정리한 글입니다.... 🐿️
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);
객체지향은 데이터를 우선적으로 준비하고 메서드들을 나중에 작성해 나간다면, 함수형 프로그래밍은 이미 만들어져 있는 함수들의 조합으로 데이터를 구성한다.