함수형 패러다임의 꽃: 함수 합성(composition)

함수형 패러다임에서 최우선 설계 원칙으로 삼아진다고 하는 함수들의 합성에 대해서 설명한다.

합성

이전에 설명했던 compose 함수를 말한다.

1
2
3
4
const compose = (...fns) =>
fns.reduce((f, g) => (...args) =>
f(g(...args)),
);

합성은 왜 하는걸까?

프로그램을 간결하고 실용적으로 작성할 수 있게 한다. 합성이 되므로 함수를 부담 없이 나눌 수 있게 되어 더 작고 의미있는 단위의 함수를 더 편하게 작성할 수 있다. 이렇게 합성된 함수는 가독성이 좋다. 아무래도 객체지향 패러다임을 강하게 지원하는 언어들에선 함수 합성이 쉽지 않다. 애초에 순수 함수를 작성하기도 쉽지 않다. public static으로 도배할 순 없기 때문이다.

합성함수의 결합법칙

함수 합성은 수학에서의 합성함수와 같이 결합법칙이 성립한다. compose(f, compose(g, h)) === compose(compose(f, g), h)가 성립한다. Javascript 상에서 생성되는 함수가 동일하다는 것이 아니라, 그 실행 결과가 언제나 같다는 뜻이다.

결합법칙이 무슨 소용일까

합성한 함수들을 재귀적으로 합성한 경우, 결합법칙을 적용하면 결과 예측과 리팩토링 시에 유용하다.

그 예로, 아래 세가지 loudLastUpper 함수는 동일하다. 더 작고 더 의미있는 함수로 정의할수록 재사용성과 가독성은 높아진다.

버전 1

1
2
3
4
5
6
const loudLastUpper = compose(
exclaim,
toUpperCase,
head,
reverse,
);

버전 2 (리팩토링)

1
2
3
4
5
6
const last = compose(head, reverse);
const loudLastUpper = compose(
exclaim,
toUpperCase,
last,
);

버전 3 (리팩토링)

1
2
3
const last = compose(head, reverse);
const angry = compose(exclaim, toUpperCase);
const loudLastUpper = compose(angry, last);

쓸모있고 재미있는 디버깅 방법

합성 함수를 디버깅하는 재밌는 방법이 있다. 흔히 trace라 부르는 유명한 함수인데, 항등함수(const pass = x => x;)에 console.log만 추가한 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const trace = (tag) => (x) => {
console.log(tag, x);
return x;
};

// 자동 커리
const trace = curry((tag, x) => {
console.log(tag, x);
return x;
});

// 사용 예시
const toDebug = compose(
replace,
trace('after A'),
applyA,
trace('after B'),
applyB,
trace('after last'),
last,
);

당연하게도 trace 함수는 순수하지 않다. console를 사용하기 때문이다.

간단한 함수 합성 예제

이하의 예제 코드는 아래의 cars 객체를 대상으로 한다.

1
2
3
4
5
6
7
8
9
[
{
name: 'Aston Martin One-77',
horsepower: 750,
dollar_value: 1850000,
in_stock: true,
},
//...
];

예제 1

각 함수들의 정의는 이 문서를 참고하라. 이 문서는 ramdajs documentation과도 호환된다.

1
2
3
4
5
6
7
8
9
10
const isLastInStock = (cars) => {
const lastCar = last(cars);
return prop('in_stock', lastCar);
};

// after compose:
const isLastInStock = compose(
prop('in_stock'),
last,
);

예제 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const average = (xs) =>
reduce(add, 0, xs) / xs.length;

const averageDollarValue = (cars) => {
const dollarValues = map(
(c) => c.dollar_value,
cars,
);
return average(dollarValues);
};

// after compose:
const averageDollarValue = compose(
average,
map(prop('dollar_value')),
);

예제 3

1
2
3
4
5
6
7
8
9
10
11
12
13
const fastestCar = (cars) => {
const sorted = sortBy((car) => car.horsepower);
const fastest = last(sorted);
return concat(fastest.name, ' is the fastest');
};

// after compose:
const fastestCar = compose(
append(' is the fastest'),
prop('name'),
last,
sortBy(prop('horsepower')),
);

함수 합성 예제 프로그램

스펙

  1. 검색어에 대응하는 URL을 생성한다.

  2. flicker API를 호출한다.

  3. 결과 JSON에서 이미지 링크를 추출한다.

  4. 이미지를 HTML에 표시한다.

구현 코드

예제의 스펙에서 보았듯, 2단계 API 호출과 4단계 이미지 표시는 순수하지 않다. 일단 순수하지 않은 함수를 같이 사용하면서 예제를 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 유틸 함수 선언
const prop = curry((p, obj) => obj[p]);

// 순수하지 않은 함수
// Impure 객체로 접근하도록 하여 사용자에게 주의를 준다.
const Impure = {
getJSON: curry((callback, url) =>
$.getJSON(url, callback),
),
setHtml: curry((sel, html) =>
$(sel).html(html),
),
};

/*
참고: 서버의 응답이 아래와 같은 형태로 구성됨
{
...
items: [
{
...
media: {
m: '<image-link>'
}
},
{ ... },
{ ... },
]
}
*/
const host = 'api.flicker.com';
const path = '/services/feeds/photos-public.gne';
const query = (t) =>
`?tags=${t}&format=json&jsoncallback=?`;
const url = (t) =>
`https://${host}${path}${query(t)}`;

const mediaUrl = compose(
prop('m'),
prop('media'),
);

const mediaUrls = compose(
map(mediaUrl),
prop('items'),
);

const img = (src) => `<img src="${src}" />`;

const render = compose(
Impure.setHtml('#root'),
map(img),
mediaUrls,
);

const app = compose(Impure.getJSON(render), url);

app('cat');

compose와 map 리팩토링

아주 간단한 리팩토링이다. 같은 배열에 대해 map을 여러 번 실행하기보다, 순서를 유지한 채로 매 원소에 대해 map할 함수를 합성해서 한 번에 실행하게 되면 반복 횟수를 줄일 수 있다.

1
2
3
4
// from
compose(map(img), map(mediaUrl));
// to
compose(map(compose(img, mediaUrl)));

참고

mostly-adequate-guide (EN)

함수형 패러다임의 꽃: 함수 합성(composition)

https://jsqna.com/fjs-5-composition/

Author

Seongbin Kim

Posted on

19-09-10

Updated on

21-01-19

Licensed under

댓글