L.map으로 map 만들기
이터러블 객체를 순회하면서 일괄적으로 함수를 적용하는 map 함수를 지연평가를 적용한 L.map을 통해서 구현할 수 있다.
먼저 L.map의 코드는 아래와 같다.
L.map = curry(function *(f, iter) {
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
yield f(a);
}
});
그리고 map 코드는 아래와 같다.
const map = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
res.push(f(a));
}
return res;
});
L.map과 map은 혼용이 가능한데, 그 이유는 둘 모두 이터러블한 객체를 받으며 이터러블 객체에서 이터레이터를 꺼내 순회할 수 있기 때문이다.
const map = curry((f, iter) => go(
iter,
L.map(f)
});
console.log(map(a => a + 10, [0, 1, 2, 3]));
// Generator {<suspended>}
위와 같이 작성하면 일단 논리적으로 iterable 객체를 받아 L.map을 수행할 수 있다. 여기서 값을 평가하여 실제 map 함수와 동일하게 동작할 수 있도록 구현한다.
const map = curry((f, iter) => go(
iter,
L.map(f),
take(Infinity)
});
console.log(map(a => a + 10, L.range(4)));
// (4) [10, 11, 12, 13]
iterable의 길이가 어느 정도이든 모든 값을 순회하기 때문에 기존 map과 동일하게 동작한다.
go의 첫 번째 인자인 iter를 L.map 함수에 바로 넣을 수 있다.
const map = curry((f, iter) => go(
L.map(f, iter),
take(Infinity),
});
여기서 f와 iter를 받아서 바로 L.map에 전달하기에 이를 pipe 함수로 전환할 수도 있다.
const map = curry(pipe(
L.map,
take(Infinity)
});
L.filter로 filter 함수 만들기
filter도 동일하게 iterable 객체를 받아서 로직을 수행하기 때문에 앞선 사례처럼 L.filter로 filter함수를 대체할 수 있다.
먼저 L.filter 함수는 다음과 같다.
L.filter = curry(function *(f, iter) {
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
if (f(a)) {
yield a;
}
}
});
filter 함수는 다음과 같다.
const filter = curry((f, iter) => {
let res = [];
iter = iter[Symbol.iterator]();
let cur;
while (!(cur = iter.next()).done) {
const a = cur.value;
if (f(a)) res.push(a);
}
return res;
});
전체적으로 동일한 형태를 갖고 있으며, 이터러블 객체를 받아 L.filter를 수행한 후에 take(Infinity)로 값을 모두 가져오므로, map과 동일하게 단순화할 수 있다.
const filter = curry(pipe(L.filter, take(Infinity)));
console.log(filter(a => a % 2, [0, 1, 2, 3]));
코드 단순화
take(Infinity)는 map과 filter에서 동일하게 쓰이므로 변수에 저장해 사용하는 것도 방법이다.
const takeAll = take(Infinity);
다음으로 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.map과 L.filter라는 함수형이 조금 생소하게 느껴지는데 이를 map, filter로 바꿔서 표현할 수 있어서 좋았다. 또한, 지연평가를 수행하기 때문에 Inifinity를 자유롭게 사용할 수 있다는 점도 큰 메리트였다. 이를 통해서 map, filter와 동일하게 표현할 수 있기 때문이다. 또한, curry와 pipe의 놀라운 효과에 매번 놀란다. curry는 나중에 인자를 받아서 처리할 수 있다는 첨이 가독성을 높여주는 데 많은 도움이 된다. 그리고 pipe도 마찬가지로 중복된 코드를 줄일 수 있어서 단순화에 강점이 있다. 아직 curry와 pipe의 합성 원리를 제대로 이해하지는 못했기 때문에 내부 로직을 하나하나 풀어서 이해해보고 싶다.
출처: 인프런 함수형 프로그래밍과 JavaScript ES6+
'프론트엔드 > JS' 카테고리의 다른 글
[JavaScript] L.flatMap, flatMap (0) | 2023.05.10 |
---|---|
[JavaScript] L.flatten, flatten, L.deepFlat 함수 만들기 (0) | 2023.05.09 |
[JavaScript] 함수 합성을 통해 find 함수 만들기 (0) | 2023.05.08 |
[JavaScript] Array.prototype.join 보다 다형성 높은 join 함수 (0) | 2023.05.08 |
[JavaScript] map, filter & reduce로 결과를 만드는 함수 생성 (0) | 2023.05.07 |