Skip to content

5장: 함수 조합을 통한 코딩 ​

함수 합성 ​

다음은 compose의 구현입니다:

js
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.call(null, ...res)], args)[0];

... 겁먹지 마세요! 이건 레벨 9000 슈퍼 사이언 형태의 compose입니다. 이해를 돕기 위해 가변 인자 구현을 제외하고 두 함수를 조합하는 단순한 형태부터 살펴보겠습니다. 이를 이해하고 나면 추상화를 확장해 여러 함수에 적용할 수 있습니다(증명도 가능합니다!) 친절하게 설명한 compose 예시입니다:

js
const compose2 = (f, g) => x => f(g(x));

f와 g는 함수이며 x는 이들을 통해 "흐르는" 값입니다.

합성은 함수 번식과 유사합니다. 함수 브리더로서 원하는 특성을 가진 두 함수를 선택해 새 생명체처럼 결합시킵니다. 사용 예시:

js
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 없이는 다음과 같죠:

js
const shout = x => exclaim(toUpperCase(x));

안에서 밖으로가 아닌 오른쪽에서 왼쪽으로 실행합니다(왼쪽 방향으로의 진전!). 실행 순서가 중요한 예시를 보겠습니다:

js
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 기능입니다. 함수 실행 순서가 여기서 명확히 드러납니다. 좌->우 버전도 가능하지만 수학적 정의를 따르는 게 더 적합합니다. 합성은 수학 교과서에서 온 개념입니다.

js
// associativity
compose(f, compose(g, h)) === compose(compose(f, g), h);

함수 합성은 결합법칙을 따르므로 그룹화 방식이 중요하지 않습니다. 문자열 대문자 변환 예시:

js
compose(toUpperCase, compose(head, reverse));
// or
compose(compose(toUpperCase, head), reverse);

어떻게 그룹화하든 결과는 동일합니다. 따라서 가변 인자 compose를 작성해 이렇게 사용할 수 있습니다:

js
// 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 등에서 표준으로 사용됩니다.

결합성 덕분에 함수 그룹을 추출해 독립적인 합성으로 만들 수 있습니다. 이전 예제 리팩토링:

js
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 - 포인트프리 유틸리티에 있습니다.

js
// 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를 보세요. 단일 인자 함수 체인을 통해 데이터를 전달합니다. 커링이 데이터 처리 흐름을 가능하게 합니다. 포인트프리 버전은 데이터 없이 함수를 구성할 수 있다는 장점이 있습니다.

다른 예제를 살펴봅시다.

js
// 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 함수를 부분 적용 없이 합성하는 흔한 실수

js
// 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!'])

합성 디버깅 시 유용한 (비순수) 추적 함수 사용법:

js
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 사용

js
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 적용 필요

js
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도 유사 기능 제공

합성은 강력한 이론적 기반을 가진 프로그램 구성 도구입니다. 이제 이 이론을 살펴보죠.

범주 이론 ​

범주 이론은 집합론, 타입 이론 등을 포괄하는 추상 수학 분야입니다. 객체, 사상, 변환을 다루며 프로그래밍과 밀접합니다. 다음은 각 이론별 대응표:

category theory

겁주려는 의도는 없었습니다. 범주론이 이들을 통합하려는 이유를 이해하시길.

범주의 구성 요소:

  • 객체 모음
  • 사상 모음
  • 사상 합성 개념
  • 항등 사상

현재는 타입과 함수에 초점을 맞춰 범주 이론을 적용합니다.

객체 모음 데이터 타입이 객체입니다(String, Boolean 등). 타입을 가능한 값들의 집합으로 볼 수 있어 집합론 적용이 유용합니다.

사상 모음 일상적인 순수 함수들입니다.

사상 합성 우리의 compose 도구입니다. 결합법칙은 범주론의 필수 속성입니다.

합성 과정 도식:

category composition 1category composition 2

코드 예시:

js
const g = x => x.length;
const f = x => x === 4;
const isFourLetterWord = compose(f, g);

항등 사상 입력을 그대로 반환하는 id 함수:

js
const id = x => x;

"왜 필요한가요?" 앞으로 많이 사용될 것이며, 데이터 대체 역할을 하는 함수입니다.

단항 함수 f에 대한 id의 속성:

js
// identity
compose(id, f) === compose(f, id) === f;
// true

수학의 항등원과 유사합니다. 포인트프리 코딩에서 유용하게 사용됩니다.

타입과 함수의 범주를 살펴봤습니다. 합성의 결합법칙과 항등원 속성을 이해하셨으면 합니다.

범주의 다른 예시: 노드(객체), 엣지(사상)의 유향 그래프, 숫자와 >= 관계 등. 본서에서는 기본 범주에 집중합니다.

요약 ​

합성은 파이프라인처럼 함수를 연결합니다. 이 연결이 중단되면 소프트웨어는 무용지물이 됩니다.

합성을 최우선 설계 원칙으로 삼아야 합니다. 범주론은 아키텍처와 정확성 검증에 중요합니다.

실제 예제를 통해 이해를 다져봅시다.

6장: 예제 애플리케이션

연습문제 ​

다음 구조의 자동차 객체를 사용하는 연습문제:

js
{
  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); };


다음 함수 고려:

js
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'); };