순수함수와 curry 함수

이번 글은 쉽다(?). 순수함수에 대해 이론적으로 다루고, curry 함수를 소개한다. 다만 객체지향과의 비교, 테스트와 설계에 대한 내 생각을 공유하므로 지식이 없으면 어렵게 보일 수도 있다.

순수함수?

순수함수는 수학에서 정의하는 함수와 동일하다. 입력에 대한 출력이 항상 동일하고, 입력에 대한 출력이 항상 1가지이다. 이게 가능하기 위해선 DB, HTTP, 현재 시간 등에 의존하면 안 된다! 함수 외부의 것과 함수 내용이 전혀 연관이 없어야 한다.

부원인과 부작용

영어권에서 흔히 side-effects라고 얘기하는 부수효과부작용은 함수 밖의 코드의 상태에 영향을 주는 일을 말한다. 부수효과를 크게 부작용부원인으로 구분할 수 있다. 부작용은 숨겨진 출력이고, 부원인은 숨겨진 입력이라고 생각하면 된다. 왜 외부의 상태와 상호작용하면 안 될까? 궁금하면 계속 읽어야 된다.

숨겨진 입력

숨겨진 입력이라고 하면 뭐가 있을까? Javascript와 같이 객체지향 패러다임을 지원하는 언어의 경우는 this가 항상 함수에 전달된다. this도 숨겨진 입력이다. 또한 함수 내부에서 new Date() 등의 코드로 현재 시간에 의존하는 경우도 숨겨진 입력이라고 할 수 있다. 둘 모두 외부의 상태를 변경하기 때문이다.

숨겨진 출력

숨겨진 출력은 함수를 실행했을 때 바뀌는 모든 것이라고 할 수 있다. 순수함수 내에서는 어떤 외부의 상태도 변할 수 없으므로, 어떤 외부의 상태가 조금이라도 변경된다면 그 함수는 순수하다고 할 수 없다.

부수효과는 복잡성 빙산

왜 외부의 상태와 상호작용하면 안 될까?

순수함수가 아닌 함수의 Signature는 프로그래머가 읽더라도, 심지어 객체지향 언어의 설계 방식대로 설계했더라도 무슨 부수효과가 일어날지 알 수 없다.

캡슐화는 좋은 규칙이지만 그 구현 코드를 읽기 전까지 부수 효과를 정확히 알 순 없다.

부수효과가 왜 복잡성 빙산일까? 프로그래머가 예상한 그대로 동작하지 않는 경우 논리적 버그의 원인이 되기 때문이다. 부수 효과는 해당 코드 혹은 해당 코드와 간접적으로 연관이 있는 코드를 수정했을 때 바뀌기 또한 쉽고, 바뀌었을 때 작동하지 않게 될 확률도 높다. 그래서 객체지향 방식으로 설계를 하는 경우 회귀테스트를 그렇게 많이 작성해야 하나 보다. 응집성과 캡슐화를 생각해서 상태 의존적이고, 변경 시 서로의 영향을 받아서 깨지기 쉽기 때문이다.

그래서 함수형 패러다임에서는 공유 자체를 하지 않는 방향으로 설계하도록 지향한다. 그 결과가 순수함수이다.

순수함수가 아니면 테스트하기 힘들다

어떤 함수가 부수효과가 있는 경우 이미 그 함수는 다른 코드랑 최소한 1번은 엮여 있을 수 밖에 없다. 덕분에 그 함수를 테스트하기 위해서는 다른 코드까지 테스트할 수 밖에 없고, 이 과정에서 Blackbox Testing이 불가능해진다. 구현 상세에 외부 코드와의 연관이 존재하기 때문이다. 이 과정은 객체지향 언어로 작성한 경우 자주 발생하며 덕분에 Mock을 자주 사용하게 된다. 또한 테스트 자체도 구현 상세의 변경에 취약하게 된다.

부수효과를 제거하기, 제거했을 때의 장점

부수효과를 제거하려면 순수함수를 만들고 사용하면 된다.

모든 부작용, 부원인은 숨겨진것이기에 이를 Signature에 명시하면 된다. 이렇게 명시하는 것은 객체지향 언어에서는 응집성과 캡슐화를 위해 구현 상세로 분류하여 함수 안에 전부 집어넣는 등 지양하는 편이지만, 함수형 패러다임에서는 권장된다. 덕분에 덜 복잡해지고, 훨씬 테스트하기 쉬워지며, 추론이 훨씬 쉬워지기 때문이다.

부수효과를 완전히 제거할 수는 없다.

아무래도 웹 등 실세계의 애플리케이션은 함수 내의 수식을 한 번 계산하고 종료하는 게 목적이 아니라, 부수효과로 불리는 것들 대부분을 사용하여 목적을 달성할 수 밖에 없다. 함수형 패러다임은 이런 한계를 인정하고, 가능한 모든 곳에서 부수효과를 제거하고, 제거할 수 없을 땐 강력히 통제한다.

순수함수의 조합과 재사용성

순수함수는 그 자체의 명료함 덕분에 재사용성과 조합이 굉장히 쉽고, 많이 조합하더라도 쉽게 그 결과가 예측 가능하다. 특히 한 번에 풀 수 없는 크고 복잡한 문제를 쪼개서 작은 함수의 조합으로 해결할 수 있다. 앞서 만들어 놓은 산출물을 쉽게 조합하여 새로운 문제를 해결할 수 있게 되고, 생산성도 비약적으로 늘어난다.

순수함수에 대한 간단한 사실들

  1. 순수함수는 수학의 함수와 동일한 정의를 갖는다.

  2. 순수함수는 (input, output) 쌍이므로 객체로도 표현 가능하다. (key,value 쌍)

  3. 순수함수는 항상 캐시 가능하다.

  4. 순수함수는 필요한 건 다 전달받는다(dependency injection)

  5. 동시성 문제가 적거나 없다. 공유하는 메모리가 없기 때문이다.

curry 함수

2번째 글에서 currying을 이미 다루었다. 그 때의 currying은 프로그래머가 함수에 대해 직접 curry한 방식이고, 이번에는 어떤 함수에 대해 알아서 curry된 함수를 반환하는 함수를 소개한다.

curry 함수는 함수를 받아, 인자가 완전히 전달되지 않은 경우 남은 인자를 받을 함수를 반환한다. curry 함수의 구현은 function.lengthbind, apply를 사용하는 게 핵심이다.

1
2
3
4
5
6
7
8
9
10
11
function curry(f) {
const len = f.length;
return function $curry() {
if (arguments.length < len) {
// 원래 함수의 매개변수의 갯수보다 $curry에 전달된 매개변수의 갯수가 작은 경우.
return $curry.bind(null, ...arguments); // $curry에 계속 전달받은 매개변수들이 bind 된다. (arguments가 계속 쌓인다.)
} else {
return f.apply(null, arguments); // 실제 함수 호출.
}
};
}
1
2
3
4
5
6
7
8
9
10
// 예시
const add = (a, b, c) => a + b + c;
const addC = curry(add);
const add1 = addC(1);
const add1and2 = add1(2);
const add1and2and3 = add1and2(3);
console.log(addC(1, 2)); // function $curry
console.log(add1(2)); // function $curry
console.log(add1and2and3); // 6
console.log(add1and2(3) === add1and2and3); // true

참고

ES6 bind 함수 (KO)

Author

Seongbin Kim

Posted on

19-09-06

Updated on

21-01-19

Licensed under

댓글