5장: 함수 조합을 통한 코딩
함수 합성
다음은 compose의 구현입니다:
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];... 겁먹지 마세요! 이건 레벨 9000 슈퍼 사이언 형태의 compose입니다. 이해를 돕기 위해 가변 인자 구현을 제외하고 두 함수를 조합하는 단순한 형태부터 살펴보겠습니다. 이를 이해하고 나면 추상화를 확장해 여러 함수에 적용할 수 있습니다(증명도 가능합니다!) 친절하게 설명한 compose 예시입니다:
const compose2 = (f, g) => x => f(g(x));f와 g는 함수이며 x는 이들을 통해 "흐르는" 값입니다.
합성은 함수 번식과 유사합니다. 함수 브리더로서 원하는 특성을 가진 두 함수를 선택해 새 생명체처럼 결합시킵니다. 사용 예시:
const toUpperCase = x => x.toUpperCase();
const exclaim = x => `${x}!`;
const shout = compose(exclaim, toUpperCase);
shout('send in the clowns'); // "SEND IN THE CLOWNS!"두 함수의 합성은 새로운 함수를 반환합니다. 특정 타입(여기서 함수)의 두 단위를 결합하면 동일 타입의 새 단위가 생성된다는 논리입니다. 레고 조각을 연결해 집짓기 블록을 만들 수 없는 것처럼요. 여기에는 근본적인 이론이 숨겨져 있습니다.
우리의 compose 정의에서 g는 f보다 먼저 실행되며 데이터가 오른쪽에서 왼쪽으로 흐릅니다. 중첩된 함수 호출보다 읽기 편한 구조입니다. compose 없이는 다음과 같죠:
const shout = x => exclaim(toUpperCase(x));안에서 밖으로가 아닌 오른쪽에서 왼쪽으로 실행합니다(왼쪽 방향으로의 진전!). 실행 순서가 중요한 예시를 보겠습니다:
const head = x => x[0];
const reverse = reduce((acc, x) => [x, ...acc], []);
const last = compose(head, reverse);
last(['jumpkick', 'roundhouse', 'uppercut']); // 'uppercut'reverse는 리스트를 뒤집고 head는 첫 항목을 가져옵니다. 비효율적이지만 효과적인 last 기능입니다. 함수 실행 순서가 여기서 명확히 드러납니다. 좌->우 버전도 가능하지만 수학적 정의를 따르는 게 더 적합합니다. 합성은 수학 교과서에서 온 개념입니다.
// associativity
compose(f, compose(g, h)) === compose(compose(f, g), h);함수 합성은 결합법칙을 따르므로 그룹화 방식이 중요하지 않습니다. 문자열 대문자 변환 예시:
compose(toUpperCase, compose(head, reverse));
// or
compose(compose(toUpperCase, head), reverse);어떻게 그룹화하든 결과는 동일합니다. 따라서 가변 인자 compose를 작성해 이렇게 사용할 수 있습니다:
// previously we'd have to write two composes, but since it's associative,
// we can give compose as many fn's as we like and let it decide how to group them.
const arg = ['jumpkick', 'roundhouse', 'uppercut'];
const lastUpper = compose(toUpperCase, head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
lastUpper(arg); // 'UPPERCUT'
loudLastUpper(arg); // 'UPPERCUT!'결합법칙은 유연성과 결과 일관성을 보장합니다. 복잡한 가변 인자 구현은 지원 라이브러리에 포함되어 있으며 lodash, underscore, ramda 등에서 표준으로 사용됩니다.
결합성 덕분에 함수 그룹을 추출해 독립적인 합성으로 만들 수 있습니다. 이전 예제 리팩토링:
const loudLastUpper = compose(exclaim, toUpperCase, head, reverse);
// -- or ---------------------------------------------------------------
const last = compose(head, reverse);
const loudLastUpper = compose(exclaim, toUpperCase, last);
// -- or ---------------------------------------------------------------
const last = compose(head, reverse);
const angry = compose(exclaim, toUpperCase);
const loudLastUpper = compose(angry, last);
// more variations...정답은 없습니다. 레고처럼 자유롭게 조합하세요. last나 angry처럼 재사용 가능한 그룹화가 좋습니다. Fowler의 "리팩토링"을 아신다면 객체 상태 걱정 없이 함수 추출을 하는 것과 같습니다.
포인트프리 스타일
포인트프리 스타일은 데이터를 명시하지 않는 방식입니다. 1급 함수, 커링, 합성이 이 스타일 구현의 핵심입니다.
힌트:
replace와toLowerCase의 포인트프리 버전은 부록 C - 포인트프리 유틸리티에 있습니다.
// not pointfree because we mention the data: word
const snakeCase = word => word.toLowerCase().replace(/\s+/ig, '_');
// pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);부분 적용된 replace를 보세요. 단일 인자 함수 체인을 통해 데이터를 전달합니다. 커링이 데이터 처리 흐름을 가능하게 합니다. 포인트프리 버전은 데이터 없이 함수를 구성할 수 있다는 장점이 있습니다.
다른 예제를 살펴봅시다.
// not pointfree because we mention the data: name
const initials = name => name.split(' ').map(compose(toUpperCase, head)).join('. ');
// pointfree
// NOTE: we use 'intercalate' from the appendix instead of 'join' introduced in Chapter 09!
const initials = compose(intercalate('. '), map(compose(toUpperCase, head)), split(' '));
initials('hunter stockton thompson'); // 'H. S. T'포인트프리 코드는 간결함과 일반성을 유지합니다. 입력에서 출력으로의 작은 함수들로 구성되었는지 확인하는 좋은 지표입니다. 하지만 때로는 가독성을 해칠 수 있으니 주의가 필요합니다.
디버깅
두 인자를 가진 map 함수를 부분 적용 없이 합성하는 흔한 실수
// wrong - we end up giving angry an array and we partially applied map with who knows what.
const latin = compose(map, angry, reverse);
latin(['frog', 'eyes']); // error
// right - each function expects 1 argument.
const latin = compose(map(angry), reverse);
latin(['frog', 'eyes']); // ['EYES!', 'FROG!'])합성 디버깅 시 유용한 (비순수) 추적 함수 사용법:
const trace = curry((tag, x) => {
console.log(tag, x);
return x;
});
const dasherize = compose(
intercalate('-'),
toLower,
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire');
// TypeError: Cannot read property 'apply' of undefined문제 발생 시 trace 사용
const dasherize = compose(
intercalate('-'),
toLower,
trace('after split'),
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire');
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]배열 처리에는 toLower에 map 적용 필요
const dasherize = compose(
intercalate('-'),
map(toLower),
split(' '),
replace(/\s{2,}/ig, ' '),
);
dasherize('The world is a vampire'); // 'the-world-is-a-vampire'trace 함수로 디버깕 포인트 데이터 확인. Haskell과 PureScript도 유사 기능 제공
합성은 강력한 이론적 기반을 가진 프로그램 구성 도구입니다. 이제 이 이론을 살펴보죠.
범주 이론
범주 이론은 집합론, 타입 이론 등을 포괄하는 추상 수학 분야입니다. 객체, 사상, 변환을 다루며 프로그래밍과 밀접합니다. 다음은 각 이론별 대응표:

겁주려는 의도는 없었습니다. 범주론이 이들을 통합하려는 이유를 이해하시길.
범주의 구성 요소:
- 객체 모음
- 사상 모음
- 사상 합성 개념
- 항등 사상
현재는 타입과 함수에 초점을 맞춰 범주 이론을 적용합니다.
객체 모음 데이터 타입이 객체입니다(String, Boolean 등). 타입을 가능한 값들의 집합으로 볼 수 있어 집합론 적용이 유용합니다.
사상 모음 일상적인 순수 함수들입니다.
사상 합성 우리의 compose 도구입니다. 결합법칙은 범주론의 필수 속성입니다.
합성 과정 도식:


코드 예시:
const g = x => x.length;
const f = x => x === 4;
const isFourLetterWord = compose(f, g);항등 사상 입력을 그대로 반환하는 id 함수:
const id = x => x;"왜 필요한가요?" 앞으로 많이 사용될 것이며, 데이터 대체 역할을 하는 함수입니다.
단항 함수 f에 대한 id의 속성:
// identity
compose(id, f) === compose(f, id) === f;
// true수학의 항등원과 유사합니다. 포인트프리 코딩에서 유용하게 사용됩니다.
타입과 함수의 범주를 살펴봤습니다. 합성의 결합법칙과 항등원 속성을 이해하셨으면 합니다.
범주의 다른 예시: 노드(객체), 엣지(사상)의 유향 그래프, 숫자와 >= 관계 등. 본서에서는 기본 범주에 집중합니다.
요약
합성은 파이프라인처럼 함수를 연결합니다. 이 연결이 중단되면 소프트웨어는 무용지물이 됩니다.
합성을 최우선 설계 원칙으로 삼아야 합니다. 범주론은 아키텍처와 정확성 검증에 중요합니다.
실제 예제를 통해 이해를 다져봅시다.
연습문제
다음 구조의 자동차 객체를 사용하는 연습문제:
{
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
}연습해 보세요!
compose()로 다음 함수 재작성
const isLastInStock = (cars) => {
const lastCar = last(cars);
return prop('in_stock', lastCar);
};
다음 함수 고려:
const average = xs => reduce(add, 0, xs) / xs.length;연습해 보세요!
average 헬퍼로 averageDollarValue 리팩토링
const averageDollarValue = (cars) => {
const dollarValues = map(c => c.dollar_value, cars);
return average(dollarValues);
};
연습해 보세요!
fastestCar 포인트프리 스타일 리팩토링 (append 활용)
const fastestCar = (cars) => {
const sorted = sortBy(car => car.horsepower, cars);
const fastest = last(sorted);
return concat(fastest.name, ' is the fastest');
};