Node.js/Javascript

[함수형프로그래밍] 3. Reduce 함수 ( with Test )

후뿡이 2024. 4. 1. 21:08

이 강의는 Inflearn 유인동 님의 강의 함수형 프로그래밍과 Javacsript ES6+ 를 수강하고 작성한 글입니다.

 

🐳 함수형 프로그래밍의 기초 Map, Filter, Reduce 그 중에서도 Reduce

함수형 프로그래밍은 여러 함수를 조합해 값을 다루는 패러다임이다.

그 중에서도 함수형 프로그래밍의 근간이 되는 세 가지 함수를 꼽으라면 map, filter, reduce가 있을 것이다.

 

map과 filter는 함수형 프로그래밍을 주로 사용하지 않더라도 자주 사용해 보았을 것이다.

map(f,iter)은 이터러블 객체를 순회하면서 iter.next() 의 값에 f를 적용해 새로운 iterable 객체를 리턴한다.
filter(f,iter)는 이터러블 객체를 순회하면 f(iter.nex())를 만족하는 값만 걸러 새로운 iterabler 객체를 리턴한다.

 

하지만 reduce는 뭔지 잘 모를 수도 있다.

 

오늘은 세 가지 근간 함수 중에 reduce에 대해 알아보자 !

🐳 기본 Reduce 함수

함수형 프로그래밍에서는 map과 filter가 함수를 적용해 새로운 iterable 객체를 리턴했다면

reduce는 iterable을 평가해 값을 축약하는 함수이다.

 

코드와 함께 본다면 더 이해하기 쉬울 것이다.

// f는 적용할 함수, acc는 입력받는 초기값이자 누적값, iter 는 iterable 객체
const reduce = (f,acc,iter) => {
    for (const a of iter) {
        acc = f(acc, a);
    }
    return acc
}

 

위의 코드를 살펴보자

iter 객체를 for...of 반복문으로 순회하면서

acc에 f(acc,a)를 누적하는 것이다.

 

이렇게만 보면 이해가 잘 가지 않을 수 있으니 위의 reduce를 사용한 예제를 만들어보자.

두 수를 더하는 함수은 add 함수를 만들어 reduce에 적용해 보자 !

 

const add = (a, b) => a + b;

// range함수는 (start,end,step)으로 start이상 end 미만의 수를 step 단위로 생성하는 이터러블 객체다.
console.log(reduce(add, 0, range(1,10,1)));

 

( range 함수가 궁금하시다면 여기를 클릭해 주세요. )

 

위의 코드를 살펴보면

acc : 초기값은 0 

iter : range(1,10,1)로 1~9까지의 값을 리턴할 것이다.

 

그렇다면 결과는 어떻게 될까 ?

답은 45일 것이다.

 

그 과정을 reduce 함수의 과정과 함께 살펴보자

 

const reduce = (add,0,range(1,10,1)) => {
    for (const num of range(1,10,1)) {
        acc = add(acc, num);
    }
    return acc
}

 

reduce 함수에 add, 0, range을 인자로 전달한 코드를 살펴보자.

range(1,10,1)을 순회하면서 acc 값을 add(acc,num) 으로 갱신하고 있다.

 

처음 num = 1이 될 것이고 acc = 0 이다. add를 적용한 결과 값은 1이 될 것이다.

그다음 num = 2 이고 acc = 1 이다. add를 적용한 결과 값은 3이 될 것이다.

그다음 num = 3 이고 acc = 3 이다. add를 적용한 결과 값은 6이 될 것이다.

그다음 num = 4 이고 acc = 6 이다. add를 적용한 결과 값은 10이 될 것이다.

 

이 과정을 num = 9 일 때까지 적용한 결과 reduce 값은 45가 되는 것이다.

 

이처럼 reduce함수는 함수, 초기값(누적값), 이터러블 객체를 인자로 받아

이터러블 객체를 순회하면서서 f ( acc, iter.next()) 를 적용해 누적값을 갱신한 후

누적값을 리턴하는 것이다.

 

이러한 특성 때문에 reduce 함수는 map, filter와는 다르게 함수를 축약해 값을 만드는 함수라고 하는 것이다.

 

🐳 Reduce 함수 고도화

위의 reduce 함수는 인자를 세 개를 받았다. 함수, 초기값, iterable 객체

 

그런데 여기서 초기값을 주지 않고 iterable의 첫번째 값을 초기값으로 사용할 수는 없을까 ?

 

위의 reduce 함수를 초기값이 없는 경우 iterable의 첫 번째 값을 초기값으로 사용하도록 수정해 봅시다.

 

const reduce = (f,acc,iter) => {
    // iter가 없을 때는 acc가 iterable이라고 가정
    // iter를 이터레이터로 변환 후 acc에 iterator의 첫번째 값을 꺼내 대입
    if (!iter) {
        iter = acc[Symbol.iterator]();
        acc = iter.next().value;
    }
    for (const a of iter) {
        acc = f(acc, a);
    }
    return acc
}

 

위의 코드의 !iter 부분 부터 보면

iter가 없는 경우, 즉 인자가 2개만 들어온 경우에는 ( 이 때 acc는 iterable하다고 가정합시다 )

iter 를 iterable한 객체인 acc의 Symbol.iterator 를 실행해 이터레이터 객체로 만들어 주고

그 iter의 첫번째 값인 iter.next().value를 acc의 값에 대입해 준다.

 

이렇게 코드를 구현해 주면 reduce 함수에 인자로 함수와 이터러블 객체만 주면

초기값을 이터러블 객체의 첫번째 값으로 설정한 후에 reduce함수를 자동으로 실행해 준다.

 

🐳 Reduce 함수 테스트 코드 작성

우리가 예상했던 결과들이 잘 도출 되는지 테스트코드를 작성해 봅시다 !

 

const { reduce, add, range } = require('./fp');

describe('reduce function', () => {
    it('iterable을 순회하며 값을 축약한다.', () => {
        const sum = reduce(add, 0, range(1, 10, 1));
        expect(sum).toBe(45);
    });

    it('초기값이 주어지지 않은 경우 iterable객체의 첫 번째 값을 초기값으로 사용한다.', () => {
        const sum = reduce(add, [1,2,3,4,5]);
        expect(sum).toBe(15);
    });
});

 

 

초기값이 있는 경우와 없는 경우의 두 가지 테스트 코드를 모두 테스트 해봅시다.

처음 테스트 코드는 iterable한 객체인 range를 입력 받아 초기값 0 부터 1~9의 수를 더한 값을 출력합니다.

두 번째 테스트 코드는 초기값을 입력받지 않고 reduce함수를 실행합니다.

이 경우 위의 코드에서 의도한 것처럼 1이 첫 번째 값이 되고 2,3,4,5를 더해 15의 값이 나와야 합니다.

 

테스트 코드를 실행해 봅시다.

 

모두 예상한 결과값이 나오는 것을 확인할 수 있습니다.

 

🐳 요약

1. reduce함수는 값을 축약하는 함수이다.

2. 초기 값이 없는 경우 iterable 객체의 첫 번째 값을 초기값으로 사용하게 할 수 있다.