본문 바로가기

Backend/함수형 자바스크립트

Underscore.js - _.reduce

이전 포스팅 참고

 

- _.reduce

 

native javascript 의 Array.prototype.reduce 메서드나 Underscore.js 의 _.reduce 함수 모두 값의 리스트를 하나의 값으로 만드는 기능을 합니다. 먼저 예시를 몇 개 보도록 하겠습니다.

 

const _ = require('underscore');

const test1 = [1, 2, 3, 4].reduce((acc, cur) => acc + cur, 0);
console.info(test1); // 10

const test2 = _.reduce([5, 6, 7, 8], function(memo, val) { return memo + val; }, 0);
console.info(test2); // 26

 

사용법은 둘 다 비슷합니다. 배열을 순차적으로 돌면서 acc 혹은 memo 에 누적값을 저장하고, cur 혹은 val 은 현재 인덱스의 값을 가리킵니다. 0 은 initialValue 라고 해서 누적값의 초기값입니다. 이 값이 없으면 native javascript 의 reduce 는 빈 배열일 경우 TypeError 가 발생합니다. 

 

const _ = require('underscore');

const test1 = [].reduce((acc, cur) => acc + cur);
console.info(test1); // TypeError

const test2 = _.reduce([], function(memo, val) { return memo + val; });
console.info(test2); // undefined

 

underscore 는 역시나 이 함수도 에러 없이 돌아가고, undefined 를 반환합니다.

생각보다 저는 실무에서 map, filter 보다 reduce 를 더 자주 쓰는 것 같습니다. 이전 포스팅에서도 가끔 나왔던 것처럼 map, filter 를 둘 다 사용해야 하는 경우에도 reduce 하나로 줄일 수 있습니다.

 

const users = [
  { id: 1, age: 30 },
  { id: 2, age: 29 },
  { id: 3, age: 11 },
  { id: 4, age: 58 },
  { id: 5, age: 68 },
  { id: 6, age: 43 }
];

// 나이가 30세 이상인 user 의 id 만 리스트로 뽑을 때
const userIds1 = users.filter(item => item.age >= 30).map(item => item.id);
console.info(userIds1); // [1, 4, 5, 6]

const userIds2 = users.reduce((acc, cur) => {
  if (cur.age >= 30) {
    acc.push(cur.id);
  }
  return acc;
}, []);
console.info(userIds2); // [1, 4, 5, 6]

 

const _ = require('underscore');

const users = [
  { id: 1, age: 30 },
  { id: 2, age: 29 },
  { id: 3, age: 11 },
  { id: 4, age: 58 },
  { id: 5, age: 68 },
  { id: 6, age: 43 }
];

const userIds = _.reduce(users, function(names, user) {
  if (user.age >= 30) {
    names.push(user.id);
  }
  return names;
}, []);
console.info(userIds); // [1, 4, 5, 6]

 

map 과 filter 를 써도 동일하긴 하지만, 루프를 더 적게 돌고 새로운 Array 객체도 한 번만 만들게 되는 이점이 있습니다.

또한 reduce 가 무엇을 하는지는 iteratee 가 결정함으로써 복잡한 로직을 단순하게 작성할 수 있도록 합니다.

 

이 reduce 를 지금까지 활용했던 frame 을 사용해서 만들어보겠습니다.

좋은 방법은 아니지만, frame 을 결국 또 약간 수정해야 합니다.

 

function frame(newData, doIteratee, stopper, isReduce) {
  return function(data, iterOrPredi, initValue) {
    const result = newData(data);
    let memo = isReduce ? initValue : undefined;

    if (isArrayLike(data)) {
      for (let i = 0 ; i < data.length ; i++) {
        memo = isReduce ? iterOrPredi(memo, data[i], i, data) : iterOrPredi(data[i], i, data);
        if (!stopper) doIteratee(memo, result, data[i], i); // modified
        else if (stopper(memo)) return doIteratee(memo, result, data[i], i); // modified
      }
    } else {
      for (let i = 0, keys = underscore.keys(data) ; i < keys.length ; i++) {
        memo = isReduce ? iterOrPredi(memo, data[keys[i]], keys[i], data) : iterOrPredi(data[keys[i]], keys[i], data);
        if (!stopper) doIteratee(memo, result, data[keys[i]], keys[i]); // modified
        else if (stopper(memo)) return doIteratee(memo, result, data[keys[i]], keys[i]); // modified
      }
    }

    return isReduce ? memo : result;
  }
}

 

frame 함수가 점점 괴물이 되어가는 것 같네요. (frame 을 통해 함수를 만드는 건 여기서 끝입니다)

frame 의 파라미터 정의에 isReduce 를 추가했습니다. 네이밍에서 알 수 있듯이 reduce 는 이 값을 true 로 넣게 될 겁니다.

그에 따라 내부 구현이 조금씩 변화가 있는데, isReduce 일 때만 세번째 인자로 들어오는 초기값을 세팅하도록 설정했습니다.

이 memo 가 isReduce 일 때는 누적값이 되는 것입니다. 그리고 두번째 인자인 iterOrPredi 가 reduce 에선 누적값을 변경하는 함수가 들어오므로 memo 는 iterOrPredi 에 의해 루프를 돌면서 변경되고, doIteratee 는 _.noop 정도로 처리하면 될 것 같습니다.

 

const test1 = _.map([1, 2, 3], function(val) { return val * val; });
console.info(test1); // [1, 4, 9]
_.each([1, 2, 3], function(val) { console.info(val * 2); });
// 2
// 4
// 6

const test2 = _.filter([1, 2, 3], function(val) { return val > 2; });
console.info(test2); // [3]

const test3 = _.reject([1, 2, 3], function(val) { return val > 2; });
console.info(test3); // [1, 2]

const test4 = _.find([1, 2, 3], function(val) { return val > 2; });
console.info(test4); // 3

 

변경된 frame 이 기존의 함수에 영향을 주지는 않았는지 먼저 테스트해봤습니다. 이제 reduce 를 만들고, 확인해보겠습니다.

 

_.reduce = frame(_.noop, _.noop, undefined, true);

const test1 = _.reduce([1, 2, 3], function(memo, val) {
  return memo + val;
}, 0);
console.info(test1); // 6

const users = [
  { id: 1, age: 30 },
  { id: 2, age: 29 },
  { id: 3, age: 11 },
  { id: 4, age: 58 },
  { id: 5, age: 68 },
  { id: 6, age: 43 }
];
const test2 = _.reduce(users, function(memo, user) {
  if (user.age >= 30) {
    memo.push(user.id);
  }
  return memo;
}, []);
console.info(test2); // [1, 4, 5, 6]

 

reduce 는 초기값을 따로 넘기기 때문에 frame 에 첫 번째 인자는 _.noop 을 넣고, doIteratee 도 아무것도 하지 않아도 되니 마찬가지로 _.noop 을 넣습니다. 또한 stopper 도 필요 없기 때문에 undefined 를 넣은 후 isReduce 에 true 를 넣습니다.

(네 번째 인자로 isReduce 를 받게 구현하면서 stopper 에 어쩔 수 없이 undefined 를 넣게 되었습니다. 충분히 리팩토링 할 여지가 있는 부분입니다)

 

frame 함수를 기존의 함수까지 커버할 수 있도록 자꾸 고치면서 진행하다보니 frame 자체는 뭔가 보기 싫게 되었지만, _.reduce 또한 이제 의도한대로 정확히 동작합니다.

 

frame 함수 내부는 지극히 절차지향적이고 중복 코드도 꽤나 보입니다. 여기서 일부 순서만 변경한다면, 중복 코드 자체도 반정도 줄일 수 있을 것입니다. 여기서 하진 않겠지만, 그런 식으로 최적화한 절차지향적인 코드는 앞으로 범용적인 함수나 라이브러리 등을 만들 때 큰 도움이 될 수 있습니다. (절차지향이 나쁜건 절대 아니며, 오히려 더 적절할 때가 있습니다)

 

_.map = frame(_.emptyArray, _.pushTo);
_.each = frame(_.identity, _.noop);
_.filter = frame(_.emptyArray, _.if(_.identity, _.applyRest(_.push)));
_.reject = frame(_.emptyArray, _.if(_.not, _.applyRest(.push)));
_.find = frame(_.noop, _.applyRest(_.identity, 2), _.identity);
_.findIndex = frame(_.constant(-1), _.applyRest(_.identity, 3), _.identity);
_.findKey = frame(_.noop, _.applyRest(_.identity, 3), _.identity);
_.some = frame(_.constant(false), _.constant(true), _.identity);
_.every = frame(_.constant(true), _.constant(false), _.not);
_.reduce = frame(_.noop, _.noop, undefined, true);

 

진행하면서 많은 작은 함수를 만들었지만, frame 을 통해 조립해 본 함수만 나열해봤습니다.

이미 Underscore.js 나 Lodash.js 가 있기 때문에 이렇게 만든 함수들을 실제로 사용할 일은 없을 것입니다.

다만 이 과정들을 통해 함수형에 대한 아이디어 확인과 함수를 값으로 다루면서 추상화 하는 방법등을 많이 체감하게 되었습니다.

 

여기서 추가로 성능 튜닝에 대한 부분을 보고자 한다면, 함수형 자바스크립트 라이브러리인 Partial.js 혹은 FxJS 의 코드를 참고하는게 좋을 것 같습니다.

'Backend > 함수형 자바스크립트' 카테고리의 다른 글

함수 조립 (2)  (0) 2021.09.18
함수 조립 (1)  (0) 2021.09.11
Underscore.js - _.reject & _.find  (0) 2021.08.29
Underscore.js - _.filter  (0) 2021.08.21
Underscore.js - _.map & _.each  (0) 2021.08.14