logo
Nostrss
Published on

필요할 때, 딱 그만큼만 - L.map과 L.filter

Authors
Lazy Map and Filter

이번에는 map과 filter의 지연 평가 버전인 L.map과 L.filter를 구현해보려고 한다.

map과 L.map

기존 map 함수 복습

먼저 이전에 만들었던 map 함수를 다시 보자.

const map = (f, iter) => {
  let res = []
  for (const a of iter) {
    res.push(f(a))
  }
  return res
}

이 함수는 모든 요소에 함수 f를 적용해서 새 배열을 만들어 반환한다. 한 번에 다 계산하는 즉시 평가 방식이다.

const result = map((a) => a + 10, [1, 2, 3])
console.log(result) // [11, 12, 13]

L.map 구현

이제 제너레이터를 사용해서 지연 평가 버전을 만들어보자.

const L = {}
L.map = function* (f, iter) {
  for (const a of iter) {
    yield f(a)
  }
}

코드가 거의 똑같다. 다른 점은 딱 두 가지다.

  1. function*으로 제너레이터 함수로 만들었다
  2. res.push(f(a)) 대신 yield f(a)로 값을 내보낸다
const iterator = L.map((a) => a + 10, [1, 2, 3])
console.log(iterator) // L.map {<suspended>}

L.map을 호출하면 배열이 아니라 이터레이터가 나온다. 아직 아무 계산도 하지 않았다.

next()로 하나씩 꺼내보기

이터레이터의 next()를 호출하면 그때 비로소 계산이 일어난다. 즉, 순회할 때마다 계산을 한다는 의미다.

const iterator = L.map((a) => a + 10, [1, 2, 3])

console.log(iterator.next()) // { value: 11, done: false }
console.log(iterator.next()) // { value: 12, done: false }
console.log(iterator.next()) // { value: 13, done: false }
console.log(iterator.next()) // { value: undefined, done: true }

next()를 호출할 때마다 하나씩 계산해서 준다. 첫 번째 next()에서 1 + 10 = 11을 계산하고, 두 번째 next()에서 2 + 10 = 12를 계산한다.

실행 흐름 비교

  1. 일반 map:

    • 호출 즉시 모든 요소에 함수를 적용해서 배열을 만든다
    • 배열이 완성되면 반환한다
  2. L.map:

    • 호출 했을 때는 아직 아무것도 하지 않는다 (준비만 함)
    • next()를 호출할 때마다 하나씩 계산해서 준다

filter와 L.filter

기존 filter 함수 복습

이전에 만들었던 filter 함수도 다시 보자.

const filter = (f, iter) => {
  let res = []
  for (const a of iter) {
    if (f(a)) res.push(a)
  }
  return res
}

조건 함수 f가 true를 반환하는 요소만 모아서 새 배열을 만들어 반환한다.

const result = filter((a) => a % 2, [1, 2, 3, 4])
console.log(result) // [1, 3]

L.filter 구현

마찬가지로 제너레이터로 바꿔보자.

L.filter = function* (f, iter) {
  for (const a of iter) {
    if (f(a)) yield a
  }
}

역시 거의 똑같다. res.push(a) 대신 yield a를 사용했다.

const iterator = L.filter((a) => a % 2, [1, 2, 3, 4])
console.log(iterator) // L.filter {<suspended>}

역시 호출하면 이터레이터가 나오고, 아직 아무 계산도 하지 않았다.

next()로 하나씩 꺼내보기

const iterator = L.filter((a) => a % 2, [1, 2, 3, 4])

console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }

next()를 호출할 때마다 조건에 맞는 값을 하나씩 찾아서 준다.

첫 번째 next()에서는 1이 조건 a % 2를 만족하므로 바로 1을 내보낸다. 두 번째 next()에서는 2를 검사하고(조건 불만족, 넘어감), 3을 검사해서(조건 만족) 3을 내보낸다.

실행 흐름 비교

  1. 일반 filter:

    • 모든 요소를 검사해서 조건에 맞는 것만 배열에 모은다
    • 배열이 완성되면 반환한다
  2. L.filter:

    • 아직 아무것도 하지 않는다 (준비만 함)
    • next()를 호출하면 조건에 맞는 값이 나올 때까지 검사하고, 찾으면 그것 하나만 내보낸다

왜 지연 평가가 중요한가?

지금까지 본 L.map과 L.filter는 단독으로 쓸 때는 그다지 차이가 없어 보인다. 하지만 이들을 조합할 때 진가가 발휘된다.

핵심은 "부르면 그때 계산한다" 는 것이다. 미리 다 만들어두지 않고, 요청이 있을 때만 동작한다는 것이다.

출처

인프런 함수형 프로그래밍과 JavaScript ES6+ 강의를 학습하고 정리한 내용입니다.

함수형 프로그래밍과 JavaScript ES6+