가장 유명한 알고리즘 중 하나는 분할정복이다. 말 그대로, 문제를 잘게 쪼개여 풀어내는 것이다. 우리가 무심코 사용하는 복잡한 함수는 실제로는 여러가지 함수로 쪼갤 수 있는 경우가 잦다. 다음과 같은 예시를 생각해보자.
function calculator(ns) {
let x = 0;
for (let i = 0; i < ns.length; i++) {
ns[i] = ns[i] * 3;
}
for (let i = 0; i < ns.length; i++) {
ns[i] = ns[i] + 2;
}
for (let i = 0; i < ns.length; i++) {
x = x + ns[i]
}
return z;
}
위 예시는 다음과 같이 고쳐서 쓸 수 있다.
ns.map(i => i * 3)
.map(i => i + 2)
.reduce((a, i) => a + i)
어떤가, 좀 더 간결한 것이 느껴지는가? 위 식은 계산의 본질만 담아내고 있다. for-loop
도 계산의 본질과는 떨어져 있다. 위 식의 좋은 점은 또 하나 있다. 바로 상태가 없다
는 것이다.
질문을 바꾸어보자. 어째서 상태는 해로운가?
우선 데이터 레이스를 유발할 수 있다. 이를 해결하기 위해서는 공유되는 자원의 소유권을 취득하고, 잠그고하는 등의 복잡한 과정이 필요하다. 하지만 더 단순한 해결방법이 있다. 공유되는 자원을 없애는 것이다.
또 다른 단점은 테스트하기 어렵다는 것이다. 우리의 함수가 참조투명하다면, 테스트하는 것은 정말 쉬운 일이 된다. 다음 코드 조각을 보자.
let x: number = 0
function pure(a: number, b: number): number {
return a + b
}
function impure(a: number, b: number): number {
x = x + 1
return a + b + x
}
pure(1, 2) // 3
pure(2, 3) // 5
impure(1, 2) // 4
impure(1, 2) // 5
impure(1, 2) // 6
어떤 함수가 더 테스트하기 쉬울까? 답은 명확하다. 어떤 부수효과를 일으키거나 외부의 존재에 의존하는 함수는 테스트하기 아주 어렵다. 생각해보라. 수많은 Mocking 프레임워크로 가득찬 우리의 테스트들. 반면 순수한 함수로 구성되어 있다면 테스트하기 훨씬 쉬워진다.
위와 더불어. 참조 투명한 함수는 최적화가 쉽다. 사람의 손을 타는 것뿐만 아니라, 기계가 읽기에도 훨씬 쉽다. 위의 예제를 생각해보라. pure(1, 2)
는 상수 3
으로 대체할 수 있으며, 아무 부작용이 없다고 단언할 수 있다. 하지만, impure
는 어떠한가? impure(1, 2)
를 3
으로 바꿀 수 있을까?
답은 아니오이다.
이제 우리가 해야할 것은 명백하다. 거대한 함수를 여러 개의 순수한 함수로 쪼개, 각각을 테스트하고 compose
, flow
등의 고차함수로 엮어내는 것이다. 이를 통해 앞서 말한 여러 이점을 얻을 수 있다.