logo
Nostrss
Published on

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

Authors
Lazy Map and Filter

이번에는 mapfilter의 지연 평가 버전인 L.mapL.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
}

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

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.mapL.filter는 단독으로 쓸 때는 그다지 차이가 없어 보인다. 하지만 이들을 조합할 때 진가가 발휘된다.

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

출처

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

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