Frontend/JS

[JavaScript] L.map, L.filter로 map과 filter 만들기

턴태 2023. 5. 9. 07:09

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+