logo
Nostrss
Published on

디버거로 reduce, go, pipe를 까보자

Authors
Debugging Function Composition

pipe 함수란?

본격적으로 들어가기 전에 pipe가 무엇인지 짚고 넘어가자.

pipe는 여러 함수를 순차적으로 실행하는 새로운 합성 함수를 만드는 함수다.

go 함수와 비교하면 이해가 더 쉽다. go는 값을 받아서 즉시 실행하고 결과를 반환한다. 반면 pipe는 함수들만 받아서, 나중에 실행할 수 있는 새로운 함수를 반환한다.

// go: 즉시 실행 (1 -> +1 -> *2 = 4)
go(
  1,
  (n) => n + 1,
  (n) => n * 2
) // 4

// pipe: 함수 합성 (실행 대기)
const f = pipe(
  (n) => n + 1,
  (n) => n * 2
)
f(1) // 4 (나중에 실행)

이 덕분에 복잡한 로직을 작은 함수들로 쪼개고, 이를 조립해서 재사용 가능한 함수를 만들 수 있다. 가독성은 덤이다.

자, 이제 이 pipe가 실제로 어떻게 동작하는지 뜯어보자.

코드가 실행될 때 각 변수에 어떤 값이 들어가는지, 마치 디버거를 돌리는 것처럼 한 땀 한 땀 추적해 보자. 이해가 안 가던 부분도 이렇게 뜯어보면 명확해진다.

하지만 pipe 함수만 봤을땐 크게 어렵지 않았었는데, 지금까지 작성했던 reduce, go를 동시에 사용해서 작성하는 코드를 작성해보니 생각보다 이해하는데 오랜 시간이 걸렸다.

아래는 reduce, go, pipe를 전부 사용한 코드인데, 어떻게 실행이 되는지 단계별로 뜯어봤다.

const log = console.log

const reduce = (f, acc, iter) => {
  if (!iter) {
    iter = acc[Symbol.iterator]()
    // 1번째 값으로 초기화
    acc = iter.next().value
  }
  for (const a of iter) {
    acc = f(acc, a)
  }
  return acc
}

const go = (...args) => reduce((a, f) => f(a), args)

// 함수를 reduce로 연속적으로 실행, pipe는 함수를 return
const pipe =
  (f, ...fs) =>
  (...as) =>
    go(f(...as), ...fs)

const definedFunc = pipe(
  (a, b) => a + b,
  (n) => n * 2,
  (n) => n - 1
)

log(definedFunc(10, 20))

log(definedFunc(10, 20))이 어떻게 결과값이 나오는지 살펴보자.

1단계: definedFunc 호출 ⭐

가장 먼저 definedFunc(10, 20)이 호출된다. definedFuncpipe 함수가 만들어낸 합성 함수다.

pipe 함수 다시보기

const pipe =
  (f, ...fs) =>
  (...as) =>
    go(f(...as), ...fs)

definedFunc가 만들어질 때 이미 ffs는 정해졌다.

  • f: (a, b) => a + b (첫 번째 함수)
  • fs: [(n) => n * 2, (n) => n - 1] (나머지 함수들의 배열)
  • as: [10, 20] (지금 들어온 인자들)

내부 실행 흐름

  1. pipe 내부에서 가장 먼저 f(...as)가 실행된다.
    • (a, b) => a + b 함수에 인자 10, 20이 들어간다.
    • 계산 결과: 10 + 20 = 30
  2. 그리고 나서 go 함수를 호출한다.
    • 첫 번째 인자로 방금 계산한 30 을 넘긴다.
    • 나머지 인자로는 아까 받아둔 함수들(...fs)을 쫙 펼쳐서 넘긴다.

결국 go(30, (n) => n * 2, (n) => n - 1) 이 실행되는 셈이다.

2단계: go 함수 실행

이제 바톤은 go 함수로 넘어왔다.

go 함수 정의

const go = (...args) => reduce((a, f) => f(a), args)

go(30, (n) => n * 2, (n) => n - 1) 이렇게 호출됐으니:

  • args[30, (n) => n * 2, (n) => n - 1] 이라는 배열이 된다.

go는 망설임 없이 바로 reduce를 호출한다.

  • f (Reducer): (a, f) => f(a) (값을 받아서 함수에 넣어주는 녀석)
  • acc (초기값 자리): args 배열 전체 ([30, func1, func2])
  • iter: 전달 안 함 (undefined)

즉, reduce((a, f) => f(a), [30, func1, func2]) 가 실행된다.

3단계: reduce 초기화 ⭐⭐⭐

여기가 가장 헷갈릴 수 있는 부분이다. reduce 내부를 현미경으로 들여다보자.

const reduce = (f, acc, iter) => {
  if (!iter) {
    // 1. iter가 없으면 acc(두 번째 인자)를 Iterable로 취급
    iter = acc[Symbol.iterator]()
    // 2. 이터레이터에서 첫 번째 값을 꺼내서 초기값(acc)으로 설정
    acc = iter.next().value
  }
  // ...

상태 변화 추적

  1. iterundefined니까 if (!iter) 조건문 안으로 들어간다.
  2. acc였던 args 배열([30, func1, func2])에서 이터레이터를 뽑아낸다.
  3. iter.next().value를 호출한다.
    • 배열의 첫 번째 값인 30 이 튀어나온다.
    • 30 이 진짜 acc(누산값)가 된다!
    • 이터레이터 포인터는 이제 두 번째 요소((n) => n * 2)를 가리키고 있다.

이 로직 덕분에 go 함수가 첫 번째 인자를 초기값으로 자연스럽게 사용할 수 있는 것이다.

4단계: reduce 루프 실행 (첫 번째 반복)

이제 for 루프가 돌기 시작한다.

for (const a of iter) {
  acc = f(acc, a)
}

1번째 턴

  • a (현재 요소): 이터레이터가 가리키던 다음 녀석, (n) => n * 2 함수다.
  • acc (누적값): 아까 꺼낸 30이다.
  • f (Reducer): go가 넘겨준 (a, f) => f(a) 함수다.

acc = f(acc, a)가 실행된다.

  • f(30, (n) => n * 2) 호출.
  • (n) => n * 2 함수에 30을 집어넣는다.
  • 계산: 30 * 2 = 60
  • 새로운 acc60이 된다.

5단계: reduce 루프 실행 (두 번째 반복)

루프는 멈추지 않는다.

2번째 턴

  • a (현재 요소): 그 다음 녀석, (n) => n - 1 함수다.
  • acc (누적값): 방금 계산한 60이다.

acc = f(acc, a) 실행.

  • f(60, (n) => n - 1) 호출.
  • (n) => n - 160을 넣는다.
  • 계산: 60 - 1 = 59
  • 새로운 acc59가 된다.

6단계: 실행 완료 및 반환

더 이상 이터레이터에 남은 게 없다. 루프 끝!

return acc
  1. reduce는 최종값 59 를 반환한다.
  2. go도 이 값을 받아서 그대로 반환한다. (59)
  3. definedFunc 호출 결과도 59 가 된다.
  4. 결국 log에는 59 가 찍히게 된다.

정리

  1. definedFunc: 인자들을 받아서 첫 함수를 실행하고, 그 결과값(30)과 나머지 함수들을 go에게 토스한다.
  2. go: reduce에게 "자, 초기값은 이거(30)고, 나머지 함수들 차례대로 실행해줘"라고 시킨다.
  3. reduce: 초기값을 들고 함수들을 하나씩 깨면서 값을 누적(accumulate) 시켜 나간다.
    • 30 -> * 2 -> 60
    • 60 -> - 1 -> 59

이렇게 뜯어보니까 gopipe, 그리고 reduce가 어떻게 맞물려 돌아가는지 좀 한눈에 들어온다.

아직은 이와 같은 패턴을 머리가 거부하는 것 같은데, 빨리 친해지기 위해 자주 사용해봐야 할 것 같다.

참고 자료

출처

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

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