본문 바로가기

Backend/함수형 자바스크립트

Underscore.js - _.reject & _.find

이전 포스팅 참고

 

- _.reject

 

이전 포스팅에서 _.filter 를 만들어봤었는데, 그 최종 형태는 아래와 같았습니다.

 

_.filter = frame(_.emptyArray, _.if(_.identity, _.applyRest(_.push)));

 

이번에 만들어 볼 _.reject 는 _.filter 와 반대로 동작하면 됩니다.

즉 _.filter 는 조건을 만족하는 값들만 모아서 새로운 배열을 만들었다면 _.reject 는 조건을 만족하지 않는 값들만 모아서 새로운 배열을 만듭니다.

 

// _.noop 사용
_.reject = frame(_.emptyArray, _.if(_.identity, _.noop, _.applyRest(_.push)));

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

// _.not 사용
_.not = function(v) { return !v; };
_.reject = frame(_.emptyArray, _.if(_.not, _.applyRest(_.push)));

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

 

두 가지 방법을 사용해봤습니다.

먼저 첫번째 방법은 만들었던 _.if 함수가 true 일때 아무것도 하지 않는 함수인 _.noop 을 넣고 false 일 때 _.push 하도록 변경한 코드입니다. 두번째는 _.identity 함수와 반대의 역할을 하는 _.not 을 만들어서 _.identity 함수를 대체시켰습니다.

함수형으로 접근하니 여러가지 방법도 보이고, 함수를 조합하는 나름의 재미가 있는 것 같습니다.

 

- _.find, _.some, _.every

 

위 함수들은 지금까지 만들었던 함수들과는 다르게 조건에 따라 루프를 중간에 나와야 합니다.

지금 frame 함수에는 그러한 기능이 없기 때문에, 조금 수정을 해보겠습니다. 물론 이 수정이 기존 _.map, _.filter 와 같은 함수에 영향을 줘선 안됩니다.

 

function frame(newData, doIteratee, stopper) { // stopper 추가 (루프를 멈추는 조건)
  return function(data, iterOrPredi) {
    const result = newData(data);
    if (isArrayLike(data)) {
      for (let i = 0 ; i < data.length ; i++) {
        const memo = iterOrPredi(data[i], i, data);
        if (!stopper) doIteratee(memo, result, data[i]);
        else if (stopper(memo)) return doIteratee(memo, result, data[i]);
      }
    } else {
      for (let i = 0, keys = underscore.keys(data) ; i < keys.length ; i++) {
        const memo = iterOrPredi(data[keys[i]], keys[i], data);
        if (!stopper) doIteratee(memo, result, data[keys[i]]);
        else if (stopper(memo)) return doIteratee(memo, result, data[keys[i]]);
      }
    }

    return result;
  }
}

_.pushTo = function(value, list) { return list.push(value); };
_.map = frame(_.emptyArray, _.pushTo);
_.each = frame(_.identity, _.noop);
_.filter = frame(_.emptyArray, _.if(_.identity, _.appyRest(_.push)));
_.reject = frame(_.emptyArray, _.if(_.not, _.applyRest(_.push)));

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

_.each([1, 2, 3, 4], function(val) { console.info(val * 3); });
// 3
// 6
// 9
// 12

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

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

 

자꾸 기존 코드였던 frame 에 기능을 추가하려고 수정하다보니 점점 코드가 더러워 지는 느낌은 있지만, 우선 수정을 해봤습니다.

stopper 를 파라미터로 넣고 루프를 빠져나올 로직을 추가했습니다.

_.find, _.some, _.every 를 만들어보기 전에 이미 만들었던 함수에는 문제가 없는지 테스트해봤고, 여기까진 문제가 없는 코드입니다. (_.pushTo 는 _.push 함수를 _.filter 와 _.reject 용으로 사용하다보니 _.map 에 맞게 새로 만들었습니다)

 

• _.find

 

Underscore.js 의 _.find 는 첫번째 인자로 들어온 collections 을 순차적으로 조회하면서 predicate 가 true 인 가장 첫번째 값을 반환하고, 만족하는 값이 없으면 undefined 를 반환합니다.

 

_.find = frame(_.noop, function(isTrue, result, val) { return val; }, _.identity);

const test1 = _.find([10, 20, 30, 40], function(val) { return val > 20; });
console.info(test1); // 30

const test2 = _.find([10, 20, 30, 40], function(val) { return val > 40; });
console.info(test2); // undefined

 

frame 함수에서 stopper 에 의해 조건을 만족하면 frame 의 두번째 인자로 넘긴 함수의 결과값이 반환되고, 조건을 만족하는 값이 없으면 undefined 가 반환되어야 하므로 frame 내부에 result 에 값을 넣을 필요가 없으니 첫번째 인자엔 아무것도 하지 않는 _.noop 을 넘겨줍니다. 만족하는 값을 반환하기 위해 두번째 인자는 val 을 반환하는 함수를 넣고, 참일때 루프를 빠져나가도록 stopper 에는 _.identity 를 넣었습니다. 이미 여러 함수를 만들어두었고 그대로 조합해서 사용하다보니 _.find 는 금방 완성되었습니다.

 

여기서 두번째 인자로 넣은 함수를 기존에 만들었던 함수로 대체할 수 있을 것 같습니다.

받은 인자(arguments) 중에 앞에 두 개는 날리고 나머지 인자를 그대로 반환하면 될 것 같은데, 이런 역할을 하는 함수를 _applyRest 로 만들어놨었습니다.

 

_.applyRest = function(func) {
  return function() {
    return func.apply(null, _.toArray(arguments).slice(1);
  }
};

 

현재의 구현은 slice 가 1 로 고정되어있어서 arguments 중 앞에 한 개만 날리도록 되어있는데, 이 값을 변수로 받을 수 있도록 변경하면 될 것 같습니다.

 

_.applyRest = function(func, num = 1) {
  return function() {
    return func.apply(null, _.toArray(arguments).slice(num);
  }
};

 

기존 _.applyRest 를 사용하는 함수들이 두 번째 인자를 넘기고있지 않으므로 별도 수정이 필요없도록 default 1 을 넣어주었습니다.

 

_.find = frame(_.noop, _.applyRest(_.identity, 2), _.identity);

 

받은 인자를 그대로 반환하는 _.identity 와 조합해서 변경했습니다. 기존의 함수와 똑같이 동작합니다.

 

• _.findIndex, _.findKey

 

그럼 위에서 만든 _.find 를 응용해서 _.findIndex, _.findKey 를 만들어보겠습니다. 함수명에서 알 수 있듯이 findIndex 를 조건을 만족하는 값의 index, _.findKey 는 값이 아닌 key 를 반환하는 함수입니다. (findIndex 는 만족하는 값이 없으면 undefined 가 아닌 -1 을 반환합니다)

 

_.findIndex 에서 index 를 반환하려면 약간의 수정이 필요합니다. 현재 frame 구현 중 doIteratee(memo, result, data[i]); 이 부분에 i 혹은 keys[i] 를 네번째 인자로 추가해준 후에, 위에서 한 것 처럼 이번엔 앞에 인자 세 개를 날리고 반환하면 될 것 같습니다.

 

function frame(newData, doIteratee, stopper) { // stopper 추가 (루프를 멈추는 조건)
  return function(data, iterOrPredi) {
    const result = newData(data);
    if (isArrayLike(data)) {
      for (let i = 0 ; i < data.length ; i++) {
        const memo = 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++) {
        const memo = 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 result;
  }
}

_.findIndex = frame(_.noop, _.applyRest(_.identity, 3), _.identity);
const test1 = _.findIndex([10, 20, 30, 40], function(val) { return val > 20; });
console.info(test1); // 2

const test2 = _.findIndex([10, 20, 30, 40], function(Val) { return val > 40; });
console.info(test2); // undefined

 

조건을 만족하는 값이 있을 때는 의도한대로 동작했는데, 값이 없을 때가 undefined 로 나오네요. -1 을 기대했는데 말이죠.

frame 첫 번째 인자에 _.noop 을 넣어서 그렇습니다. 그렇다고 그냥 -1 을 넣으면 frame 함수의 newData 가 함수로 동작하게 되어있기 때문에 TypeError 가 발생할 것이므로, -1 로 처리되는 함수를 넣어야 합니다.

 

_.constant = function(v) {
  return function() {
    return v;
  }
};

console.info(_.constant(3)); // [Function]
console.info(_.constant(3)()); // 3

 

이미 Underscore.js 에 있는 _.constant 를 만들어봤습니다. 인자로 넘긴 값을 반환하는 함수를 만들고 호출하면 그 값을 그대로 반환합니다. 이 함수를 frame 의 첫 번째 인자로 넣으면 newData(data) 에 의해 result 가 -1 로 처리될 것 같습니다.

 

_.findIndex = frame(_.constant(-1), _.applyRest(_.identity, 3), _.identity);
const test1 = _.findIndex([10, 20, 30, 40], function(val) { return val > 20; });
console.info(test1); // 2

const test2 = _.findIndex([10, 20, 30, 40], function(Val) { return val > 40; });
console.info(test2); // -1

 

단순 -1 도 함수로 처리하면서, 좀 더 함수형에 대해 느끼게 된 것 같습니다.

어찌보면 이렇게까지 해야하는가 싶기도 하지만, 모든 것을 함수로 바라보는 관점에는 정말 맞는 구현인 것 같습니다.

 

이번엔 _.findKey 입니다. 사실 이미 _.findIndex 를 수정하면서 frame 내부에 keys[i] 를 추가하는 수정을 했기 때문에, 첫 번째 인자만 다르게 처리하면 됩니다.

 

_.findKey = frame(_.noop, _.applyRest(_.identity, 3), _.identity);

const test1 = _.findKey({ id: 1, value: 3 }, function(val) { return val === 3; });
console.info(test1); // value

const test2 = _.findKey({ id: 1, value: 3 }, function(val) { return typeof val === 'string'; });
console.info(test2); // undefined

 

_.findKey 는 _.find 와 마찬가지로 조건을 만족하는 값이 없으면 undefined 를 반환하므로 _.noop 을 넣었고, 의도한대로 동작합니다.

 

• _.some

 

_.some 은 predicate 가 한 번이라도 true 면 true 를 반환하고, 모든 요소가 predicate 를 통과하지 못하면 false 를 반환합니다.

 

_.some = frame(_.constant(false), _.constant(true), _.identity);

const test1 = _.some([1, 2, 3, 4], function(val) { return val % 2 === 0; });
console.info(test1); // true

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

 

위에서 만들어뒀던 _.constant 가 유용하게 쓰이네요. 초기값은 false 여야 하니 첫 번째 인자엔 _.constant(false) 를 넣고, stopper 조건을 만족하면 doIteratee 가 true 를 반환해야 하니 _.constant(true) 를 두 번째 인자로 넣어주었습니다.

전부 의도한대로 동작합니다.

 

• _.every

 

_.every 는 모든 요소가 predicate 를 통과해야 true 를 반환하고, 하나라도 통과하지 못하면 false 를 반환합니다.

 

_.every = frame(_.constant(true), _.constant(false), _.not);

const test1 = _.every([1, 2, 3, 4], function(val) { return val > 0; });
console.info(test1); // true

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

 

_.some 과 반대로 초기값은 true 로 세팅하고 stopper 조건을 만족할 때 false 를 반환하도록 함수를 조합했습니다.

stopper 조건은 참이 아니어야 하므로 _.identity 의 반대인 _.not 을 넣은 부분이며 마찬가지로 잘 동작합니다.

 

미리 작은 함수를 만들어두고, 함수를 조합하는 일은 이미 이전 포스팅에서도 여러번 했기 때문에 크게 눈에 띄는 점은 없었습니다.

다만 개인적으로 _.constant 를 통해 단순 숫자도 함수로 만들어 인자로 넘기는 부분에서 이런 것이 함수형이구나 를 더 체감하게 되었습니다. 함수형 프로그래밍은 상황에 맞는 함수를 잘 선택해서 조합하게끔 유도하고 있는 듯 합니다.

그 과정에서 마치 캡슐화를 하듯 주요한 분기처리나 로직은 숨겨지고, 만들어 낸 함수도 frame 에 넘기는 인자들을 통해 뭘 하는지 주석처럼 읽히는 효과를 주는 것 같습니다. 좀 더 익숙해지려면 시간이 필요하겠지만, 확실히 효과적인 프로그래밍 방법인 것 같습니다.

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

함수 조립 (1)  (0) 2021.09.11
Underscore.js - _.reduce  (0) 2021.09.05
Underscore.js - _.filter  (0) 2021.08.21
Underscore.js - _.map & _.each  (0) 2021.08.14
Underscore.js - ArrayLike  (0) 2021.08.07