함수 합성은 함수들을 조합하여 새로운 함수를 만드는 것이다. 인자로 x가 주어진 함수 a의 결과값을 함수 b에 적용한다. 다시 말해, 함수 a와 함수 b를 합성한다.
만약 우리가 어떤 숫자 x에 2를 더하고 그 결과값에 3을 곱하고 싶다면, 다음과 같이 코드를 작성할 수 있을 것이다.
function add2(num) {
return num+2;
}
function multiply3(num) {
return num*3;
multiply3(add2(5)); // 21
add2()
함수와 multiply3()
함수를 합성하였다.
다음의 두 코드는 동일하게 작동한다.
// 1번 방식 - 하나의 함수로 만든다
const add2AndSquare = (num) => {
num = num+2;
num = num*num;
return num;
}
add2AndSquare(3); // 25
// 2번 방식 - 기능을 쪼개 각각 함수로 만들고, 그 함수들을 합성한 함수를 만든다
const add2 = (num) => num+2;
const square = (num) => num*num;
const add2AndSquare = (num) => sqaure(add2(num));
일단은 1번이 더 효율적으로 보인다. 1번 방식에서는 2를 더한 값을 제곱한다
라는 요구사항에 맞춘 함수를 하나만 만들었기 때문이다.
그에 비해 2번 방식은 비효율적으로 보인다. 굳이 요구사항을 기능별로 쪼개 각각의 함수를 만든 후 그 함수들을 합성한 새로운 함수를 만들었기 때문이다.
물론, 여기서 우리가 프로그램을 건들지 않는다면 1번 방식을 사용하는게 맞을 것이다. 그러나, 모든 서비스되는 프로그램은 유지보수라는 것을 한다. 모든 코드는 수정되고, 확장된다.
어느날, 위의 코드가 속한 프로그램에 2를 더한 값에 5를 더하고 그 결과를 제곱한다
는 요구사항이 들어왔다면 어떻게 될까?
아마 이렇게 코드를 짜게 될것이다.
// 1번 방식 - 새로운 함수를 만든다.
const add2AndSquare = (num) => {
num = num+2;
num = num*num;
return num;
}
const add2AndAdd5AndSquare = (num) => {
num = num+2;
num = num+5;
num = num*num;
return num;
}
add2AndSquare
와 add2AndAdd5AndSquare
는 num+5
연산 외에는 완전히 동일한 코드임에도 별도로 작성하여 코드가 중복되었다.
// 2번 방식 - 기존 코드를 재활용
const add2 = (num) => num+2;
const add5 = (num) => num+5;
const square = (num) => num*num;
const add2AndSquare = (num) => sqaure(add2(num));
const add2AndAdd5AndSquare = (num) => square(add5(add2(num));
2번 방식에서는 기존에 선언하였던 add2
와 square
함수를 활용하여 합성 함수를 만들었다.
결론적으로, 함수를 합성하는 방식을 사용하면 코드의 재활용성을 높여준다. 같은 코드를 중복해서 사용하지 않고 필요할 때마다 가져다 쓸 수 있기 때문에 유용하다.
또한 여기서 중요한 것은 합성 함수를 만들 때 사용된 각각의 함수들이 하나의 기능만을 가지고 있다는 것이다. 각각의 함수가 순수 함수일 수록, 다시 말해 하나의 기능만을 가지고, 늘 일관된 결과값을 리턴하며, 외부 함수에 영향을 미치지 않을 수록 합성하기에 적합한 함수이다. 각각의 함수가 독립적이고 간결할 때 합성하기 좋다는 사실을 명심하자.
순수함수 공부내용은 여기를 참고
compose()
와 pipe()
만들기함수를 좀 더 명시적으로 합성해주는 함수 compose()
를 만들어줄 수도 있다.
const compose = (func1, func2) => val => func2(func1(val));
const compute = compose(add2, multiply3); // f multiply3(add2(val))
compute(5); //21
compose()
함수를 좀 더 유연하게 만들어 함수를 갯수 제한 없이 받을 수도 있다.
const compose = (...funcs) => (initialVal)
=> funcs.reduceRight((val, fn) => fn(val), initialVal);
/* 이 두 코드는 완전히 같다 */
multiply5(add2(multiply3(add2(2))));
compose(multiply5, add2, multiply3, add2)(2);
Array.prototype.reduceRight
배열의 요소를 뒤에서부터 순환하며 리듀스를 수행한다.
const array1 = [[0, 1], [2, 3], [4, 5]].reduceRight(
(accumulator, currentValue) => accumulator.concat(currentValue)
);
console.log(array1); // [4, 5, 2, 3, 0, 1]
그런데 우리가 만든 compose()
함수는 맨 오른쪽 인자부터 실행된다. 이는 사람이 보기에 직관적이지 않다. 보통 왼쪽에서 오른쪽으로 가는 것이 정방향이기 때문이다. 이번에는 맨 왼쪽 인자부터 실행되는 pipe()
함수를 만들어보자.
const pipe = (...funcs) => (initialVal)
=> funcs.reduce((val, fn) => fn(val), initialVal);
/* 이 두 코드는 완전히 같다 */
multiply5(add2(multiply3(add2(2))));
pipe(add2, multiply3, add2, multiply5)(2);
잘보고 갑니다..소금같은존재 맞네요 ^^