본문 바로가기

Backend/함수형 자바스크립트

함수 조립 (1)

이전 포스팅에서 계속 해왔던 것 처럼 함수형 자바스크립트 기법을 잘 활용하면 작은 단위로 쪼갠 함수들을 조합해 큰 기능을 만들어 조합된 함수 사이에 새로운 함수를 추가하는 식으로 기능을 확장해 나갈 수 있습니다.

작은 함수를 조립해서 큰 함수를 만드는 부분에 어울릴만한 함수형 자바스크립트 라이브러리인 Partial.js 에 대해 알아보고자 합니다.

 

- 부분 적용

 

Partial.js 를 알아보기 전에, Underscore.js 에 있는 _.partial 함수에 대해 보도록 하겠습니다.

 

const _ = require('underscore');

const test1 = _.partial(console.info, 1);
test1(2); // 1 2
test1(2, 3); // 1 2 3

const test2 = _.partial(console.info, _, 2);
test2(1); // 1 2
test2(1, 3); // 1 2 3

const test3 = _.partial(console.info, _, _, 3);
test3(1); // 1 undefined 3
test3(1, 2); // 1 2 3
test3(1, 2, 4); // 1 2 3 4

const test4 = _.partial(console.info, _, 2, _, _, 5);
test4(1, 3, 4, 6); // 1 2 3 4 5 6

 

_.partial 을 사용할 때 _ 를 인자로 넣지 않은 부분에선 미리 넘긴 1의 오른쪽으로 2, 3 이 들어가고 있습니다.

_ 를 사용함으로써 부분 적용할 인자를 건너뛸 수 있습니다. _ 가 있는 자리는 이후 실행시 받은 인자로 채워지며, 들어오지 않은 자리는undefined 가 되고 다 채운 이후엔 마찬가지로 오른쪽에 인자가 들어가게 됩니다.

 

_.partial 함수를 이용하면 원하는 위치에 인자를 부분 적용할 수 있습니다. 좀 더 다양한 함수 조립 예시를 보도록 하겠습니다.

 

const _ = require('underscore');

const addAll = _.partial(_.reduce, _, function(a, b) { return a + b; });

console.info(addAll([1, 2, 3, 4])); // 10

 

Underscore.js 의 reduce 함수와 조합을 해봤습니다. _.partial 은 함수를 다루는 고차함수이며, _.reduce 함수도 고차 함수입니다. 위처럼 reduce 와 같은 고차 함수에 미리 보조 함수를 적용해둠으로써 addAll 과 같은 함수를 구현할 수 있습니다.

기존 reduce 함수를 사용할 때 initialValue 를 넣을 수 있었는데, 이를 넣고자 한다면 addAll 에 인자를 하나 추가로 넘기면 됩니다.

 

console.info(addAll([1, 2, 3, 4], 10)); // 20

 

_.partial 을 잘 이용하면, 인자를 조합하기 위해 함수로 함수를 만드는 경우를 모두 대체할 수 있습니다.

 

• _.compose

 

함수를 연속으로 실행해 주는 Underscore.js 의 compose 함수와 함께 _.partial 을 사용할 수도 있습니다.

compose 는 오른쪽 함수를 실행한 결과를 왼쪽의 함수에 전달하는 것을 반복하는 고차함수 입니다. (인자로 함수만 받습니다)

compose 의 사용법을 먼저 예시로 보도록 하겠습니다.

 

const _ = require('underscore');

const test = _.compose(console.info, function(a) { return a * 3; }, function(a) { return a + 10 });

test(0); // 30
test(10); // 60

 

인자로 들어온 0 이 _.comsose 에 인자로 들어가있는 오른쪽 함수부터 차례대로 0 -> 10 -> 30 -> console.info 의 순서로 실행되면서 30이 찍히는 코드입니다. test(10) 의 결과도 이해가 되실거라 생각합니다.

 

그럼 이 compose 함수를 _.partial 과 조합해보겠습니다.

 

const _ = require('underscore');

const isAllFalsy = _.compose(
  _.partial(_.isEqual, -1),
  _.partial(_.findIndex, _, _.identity));
  
console.info(isAllFalsy([1, true, {}])); // false
console.info(isAllFalsy([0, 1, false])); // false
console.info(isAllFalsy([0, '', false])); // true

 

compose 함수에 두번째 인자로 들어간 함수부터 보자면, _ 부분에 들어올 배열 중 identity 조건을 만족하는 첫번째 index 를 반환하도록 되어있습니다. 첫번째 인자로 들어간 함수는 -1 과 넘어온 인자가 같은지를 비교하는 함수입니다.

즉 이렇게 만들어진 isAllFalsy 함수는 인자로 받은 배열의 요소 중 하나라도 참으로 추정되는 값이 있으면 false 를 반환하고, 모든 요소가 전부 거짓으로 추정되는 값이면 true 를 반환하는 함수입니다.

 

위는 그냥 만들어본 예시일 뿐 위 케이스에선 _.every 를 사용하는게 더 간단하긴합니다. 좀 더 복잡하긴 하지만, 부분 적용과 compose 함수를 조합하니 재밌는 느낌이긴 합니다.

 

• 아쉬운 점

 

Underscore.js 의 _.partial 은 인자를 왼쪽에서부터 하나씩 적용하면서 _ 로 구분해 인자가 적용될 위치를 지정할 수 있도록 합니다.

하지만 _.partial 은 아래와 같은 상황에서 조금 아쉬운 점이 있습니다.

 

const _ = require('underscore');

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function method() {
  const iteratee = arguments[arguments.length - 1];
  arguments.length--;
  return _.reduce(arguments, iteratee);
}

console.info(method(100, 50, add)); // 150
console.info(method(100, 50, 10, add)); // 160

console.info(method(100, 50, sub)); // 50
console.info(method(100, 50, 10, sub)); // 40

const test1 = _.partial(method, _, _, _, add);

console.info(test1(1, 1, 1)); // 3
console.info(test1(1, 1)); // NaN
console.info(test1(1, 1, 1, 1)); // TypeError: iteratee is not a function

 

인자의 개수가 유동적일 경우와 조금 맞지 않는다는 점입니다. test1 에서 3개의 인자만 더할 수 있게 만들었기에 (1, 1, 1) 에는 잘 동작하지만 그 외의 케이스에선 원하는대로 동작하지 않습니다. 또한 인자 적용이 왼쪽부터 순서대로 진행되기 때문에 오른쪽부터 인자를 적용하고 싶은 경우에도 아쉬움이 있습니다.

(Lodash 에선 Underscore 의 _.partial 함수 단점을 보완하기 위해 _.partialRight 함수를 구현하기도 했지만, 약간 부족합니다)

 

• Partial.js

 

위와 같은 단점을 보완하기 위해 알아볼 Partial.js 의 _.partial 함수에는 ___ 구분자가 추가되어 있습니다.

이 함수는 기존 Underscore.js 의 _.partial 함수 기능을 모두 가지고 있습니다. 사용법을 한번 보도록 하겠습니다.

 

const _ = require('partial-js');

const test1 = _.partial(console.info, ___, 2, 3);
test1(1); // 1 2 3
test1(1, 4, 5, 6); // 1 4 5 6 2 3

const test2 = _.partial(console.info, _, 2, ___, 6);
test2(1, 3, 4, 5); // 1 2 3 4 5 6
test2(1, 3, 4, 5, 7, 8, 9); // 1 2 3 4 5 7 8 9 6

const test3 = _.partial(console.info, _, 2, ___, 5, _, 7);
test3(1); // 1 2 5 undefined 7
test3(1, 3, 4); // 1 2 3 5 4 7
test3(1, 3, 4, 6, 8); // 1 2 3 4 6 5 8 7

 

Partial.js 의 _.partial 을 실행하면 ___ 를 기준으로 왼편의 인자들을 왼쪽부터 적용하고 오른편의 인자들을 오른쪽부터 적용할 준비가 된 함수를 반환합니다.

부분적용된 함수를 나중이 실행하면 그때 들어온 인자들로 왼쪽과 오른쪽을 채운 후, 남은 인자들이 ___ 자리를 채우게 됩니다.

 

마지막 test3(1, 3, 4, 6, 8) 을 보자면 이미 하나의 인자만 들어갈 수 있게 되어 있는 두 개의 _ 부분에서 왼쪽 _ 에는 test3 를 호출할 때 가장 왼쪽에 있는 인자인 1이, 오른쪽 _ 에는 가장 오른쪽에 있는 인자인 8이 들어가게 됩니다.

그 후 나머지 3, 4, 6 이 ___ 자리를 채우게 되는 것이죠. 우선 순위는 항상 왼쪽, 오른쪽, ___ 순서 입니다.

___ 는 가변 인자를 처리할 수 있게 되어 있으므로 test3(1) 호출시 _ 부분이 undefined 가 되는것과 다르게 ___ 는 아무것도 출력되지 않습니다.

 

아까 그럼 Underscore.js 의 _.partial 을 사용해서 아쉬웠던 가변 인자 부분을 Partial.js 의 _.partial 로 변경해보겠습니다.

 

const _ = require('underscore');
const p = require('partial-js');

function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function method() {
  const iteratee = arguments[arguments.length - 1];
  arguments.length--;
  return _.reduce(arguments, iteratee);
}

const test1 = p.partial(method, ___, add);

console.info(test1(1, 1, 1)); // 3
console.info(test1(1, 1)); // 2
console.info(test1(1, 1, 1, 1)); // 4

 

Partial.js 의 _.partial 을 이용하면 인자를 조합하기 위해 함수로 함수를 만드는 모든 경우를 대체할 수 있습니다.

또한 _.chain, _.compose, _.pipeline 등 함수 합성 패턴과도 잘 어울립니다. 이 부분은 다음 포스팅에서 이어서 보도록 하겠습니다.

 

자바스크립트의 꽃이 클로저라는 말이 있는데, 그럼 결국 _.partial 가 함수형 자바스크립트의 꽃이라고 할 수 있지 않을까 싶습니다. _.partial 은 결국 클로저를 만드는 함수이기 때문입니다.

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

Partial.js (1)  (0) 2021.09.25
함수 조립 (2)  (0) 2021.09.18
Underscore.js - _.reduce  (0) 2021.09.05
Underscore.js - _.reject & _.find  (0) 2021.08.29
Underscore.js - _.filter  (0) 2021.08.21