JavaScript Reduce 함수

자바스크립트에서 고차함수(higher-order function)하면 항상 언급되는 함수가 forEach, map, reduce입니다. 그 외에도 every, some, filter와 같은 함수들도 자주 사용됩니다.

이들 고차 함수의 공통점은 함수의 인자로 함수를 받거나 함수를 리턴한다는 점입니다. filter를 예로 들면, 배열의 각 원소에 대해 callback 함수를 호출하여 true가 리턴되는 경우에만 해당 인자를 리턴 값의 배열에 포함시킵니다. 아래 예제 코드를 보면, isBigEnough 함수를 통과하는 값(10보다 크거나 같은 값)만 리턴 값에 포함되는 것을 확인할 수 있습니다.

function isBigEnough(value) {
  return value >= 10;
}
var filtered = [12, 5, 8, 130, 44].filter(isBigEnough);
// filtered is [12, 130, 44]

이런 고차함수는 아주 작은 수준의 코드 재활용으로 생각할 수 있습니다. 만약 filter라는 함수가 없었다면, 우리는 필터링 조건이 달라질 때마다 새로운 함수를 작성했을 것입니다. 예를 들어, 아래 posneg 두 함수는 배열을 인자로 받아 각각 0보다 큰 숫자, 0보다 작은 숫자만 리턴하는 함수입니다.

function pos(arr)
{
    var ret = [];
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] > 0) {
            ret.push(arr[i]);
        }
    }
    return ret;
}

function neg(arr)
{
    var ret = [];
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] < 0) {
            ret.push(arr[i]);
        }
    }
    return ret;
}

var xs = [0, -1, 2, 3, -4, 5];
console.log(pos(xs));
console.log(neg(xs));

// [ 2, 3, 5 ]
// [ -1, -4 ]

위 두 함수는 if 문 안의 조건을 제외하고는 사실상 모든 코드가 동일합니다. 따라서 이 부분만 인자로 빼내면 filter와 유사한 범용적인 함수가 나오게 됩니다. 그리고 이렇게 일단 filter 함수가 생기면 pos, neg 함수는 filter 함수를 재활용해서 쉽게 작성할 수 있게 됩니다.

function myfilter(arr, predicate) {
    var ret = [];
    for (var i = 0; i < arr.length; i++) {
        if (predicate(arr[i])) {
            ret.push(arr[i]);
        }
    }
    return ret;
}

function pos(arr) {
    return myfilter(arr, function (x) { return x > 0; });
}

function neg(arr) {
    return myfilter(arr, function (x) { return x < 0; });
}

var xs = [0, -1, 2, 3, -4, 5];
console.log(pos(xs));
console.log(neg(xs));

재미있는 건 재활용이 여기서 그치지 않습니다. posneg 함수가 서로 동일한 구조를 공유하고 있었기 때문에 filter라는 함수를 뽑아낼 수 있었던 것처럼 filter를 포함한 다른 고차함수들도 비슷한 구조를 공유하고 있기 때문에 공통점을 다시 뽑아낼 수 있습니다.

여기서 등장하는 함수가 reduce 함수입니다. reduce는 맥가이버칼처럼 여러 고차함수를 만들어낼 수 있는 표현력을 가지고 있습니다. reduce 함수는 배열의 각 원소를 돌면서 콜백 함수를 반복적으로 적용하여 하나 값을 뽑아내는 함수입니다. 아래 예제는, 배열을 돌면서 각 원소 값의 총합을 리턴하는 코드입니다.

function add(acc, value) {
  return acc + value;
}
[1, 2, 3, 4, 5].reduce(add, 0);

이 코드의 실행 결과는 add(add(add(add(add(0, 1), 2), 3), 4), 5)와 같습니다. reduce의 두 번째 인자가 acc의 초기값이 되고, 이후 각 원소를 덧셈한 결과가 계속 acc에 누적되어 넘어가는 방식입니다. 이 과정을 그림으로 표현하면 다음과 같습니다. 참고로 JavaScript의 reduce 함수는 사실 함수 프로그래밍에서는 foldl이라는 함수명으로 더 잘 알려져 있습니다.

얼핏 보기에 reduce 함수와 filter 함수는 크게 공통점이 없어 보입니다. filter 함수는 배열을 받아서 다른 배열을 리턴하는데, reduce 함수는 배열을 받아서 원소를 리턴하는 것처럼 보이기 때문입니다. 하지만 reduce 함수를 이용해 다음과 같이 filter 함수를 구현할 수 있습니다. acc에 빈 배열 []을 넘기고 predicate(x)가 참이면 해당 원소를 acc에 추가하면 최종적으로 predicate(x)를 만족하는 원소들을 리턴하게 됩니다.

function myfilter(arr, predicate) {
    return arr.reduce(function (xs, x) {
        if (predicate(x))
            xs.push(x);
        return xs;
    }, []);
}

비슷한 방식으로 map 함수도 구현할 수 있습니다.

function mymap(arr, f) {
    return arr.reduce(function (xs, x) {
        xs.push(f(x));
        return xs;
    }, []);
}

filtermap은 별개의 함수처럼 보이지만, reduce 함수를 통해 공통점을 뽑아낼 수 있는 수준의 유사점도 가지고 있음을 확인할 수 있습니다. 두 함수 모두 배열이라는 데이터 구조를 돌면서 각 원소에 대해 어떤 처리를 하고 결과값을 리턴합니다. filter는 각 원소가 predicate를 통과하는지 확인하고, 통과하는 원소들만 다시 배열로 리턴하는 반면 map은 각 원소에 함수 f를 적용하고 결과값을 조건 없이 배열로 리턴한다는 차이점이 있지만, 결과적으로 배열이라는 데이터 구조를 돌면서 결과값을 만들어내는 방식 자체는 동일한 셈입니다.

바꿔 말해, 배열로 뭔가를 해야 하는 거의 모든 함수는 reduce 함수 (혹은 reduceRight 함수)로 만들어 낼 수 있다는 뜻입니다. filtermap 외에도 underscore.js가 제공하는 대부분의 고차 함수를 reduce로 만들어 낼 수 있습니다.

fold 함수의 표현력이 궁금하신 분은 Graham Hutton 교수의 논문 A tutorial on the universality and expressiveness of fold을 읽어보시기 바랍니다.

Advertisements