함수형 프로그래밍

함수형 프로그래밍이란 무엇이고, 왜 중요한가?

수학에서 함수란 무엇인가? 수학에서 함수는 다음과 같이 나타낼 수 있다.

f(X) = Y

이 수식은 “X 를 인자로 하는 함수 f 가 있으며, 출력 Y 를 반환한다” 라고 할 수 있다. X 와 Y 에는 모든 수가 가능하다.
매우 간단한 정의지만 다음과 같은 중요한 점이 있다.

  • 함수는 인자를 가져야 한다.
  • 함수는 값을 반환해야 한다.
  • 함수는 외부가 아닌 자체 인자를 받아서만 동작한다.
  • 주어진 X 하나에 Y 는 오직 하나다.

함수형 프로그래밍 기술은 수학에서의 함수와 그 아이디어에서 발생했다. 위 정의를 가져와 자바스크립트 함수 예제를
살펴본다.

1
2
3
// 세금 계산 함수
var percentValue = 5;
var calculateTax = value => value / 100 * (100 + percentValue);

calculateTax 함수의 경우 원하는 계산을 정확히 수행한다. 즉, 값을 통해 이 함수를 호출할 수 있고, 계산된 값을 반환한다.
수학에서 함수의 핵심 정의는 함수 논리가 외부에 의존하지 않는다는 점이다. 하지만 calculateTax 함수는 전역 변수인
percentValue 에 의존한다. 따라서 이 함수는 수학적으로는 실제 함수라고 할 수 없다. percentValue 변수를 함수 인자로 이동시킨다.

1
var calculateTax = (value, percentValue) => value / 100 * (100 + percentValue);

이제 calculateTax 함수를 실제 함수라 할 수 있다. 수정해서 얻은 것은 calculateTax 함수 안에 있는 전역 변수의 접근을
막을 수 있다. 즉, 함수 안에 있는 전역 변수의 접근을 없애 테스팅을 좀 더 쉽게 할 수 있다.

함수형 프로그래밍이란 각 함수의 입력에 의존해 동작하는 함수를 생성하는 형태다. 이는 함수를 여러 번 호출했을 때도 동일한 결과를 반환할 수 있게 한다.
또한 함수 외부의 데이터 변경이 불가하므로 캐시할 수 있고, 테스트할 수 있는 코드를 작성할 수 있게 한다.

참조 투명성

함수의 정의에서 모든 함수가 동일한 입력에 대해 동일한 값을 반환받도록 선언했다. 함수의 이러한 속성을 참조 투명성 이라 한다.

참조 투명성 예제
1
var identity = i => i;

간단한 identity 함수를 정의헀다. 이 함수는 무조건 입력한 값을 전달받아 반환한다. 이 함수는 입력 인자 i에 의해서만 동작하며, 함수 내의 전역 참조는 없다.
이 함수는 참조 투명성 조건을 충족한다.

명령형, 선언형, 추상화

함수형 프로그래밍은 선언 가능하고, 추상화된 코드 작성에 관한 것이다. 이 두 개념을 이해해야 한다.

리스트와 배열이 있으며, 배열을 통해 반복적으로 콘솔에 값을 출력한다고 해보자.

1
2
3
4
var array = [1,2,3];
for(i=0; i<array.length; i++) {
console.log(array[i]);
}

정상적으로 동작하지만 문제에 접근하려고 구현했는지 정확히 알아야 한다. 예를 들어 배열 길이의 인덱스를 계산하고 내용을 출력하고자 for 문을 암묵적으로 작성했다.
여기서 작업은 배열 요소를 출력하는 것이다. 하지만 컴파일러에게 어떤 작업을 해야하는지 알려주는 것처럼 보인다. 이 경우 컴파일러에게 “배열의 길이를 가져온 후 배열을
반복하면서 인덱스를 사용해 배열의 각 요소를 얻어오라” 라고 알려주고 있다. 이를 명령형 방법이라 한다. 명령형(imperative) 프로그래밍이란 컴파일러에게 특정 작업을
어떻게 해야 하는지 알려주는 것이다.

선언형(declartive) 프로그래밍에서는 컴파일러가 어떻게 작업해야 하는지보다 어떤 것이 필요한지가 중요하다. “어떻게” 라는 부분은 일반적인 함수내에 추상화된다.

1
2
var array = [1,2,3];
array.forEach(element => console.log(element));

위의 코드는 명령형 코드와 동일한 출력을 보여준다. 하지만 여기서 “배열의 길이를 가져온 후 배열을 반복하면서 인덱스를 사용해 배열의 각 요소를 얻어오라”에서 “어떻게”
부분이 생략됐다. 추상화된 함수를 사용해서 개발자가 “어떻게” 라는 부분을 다루고, 직접 문제를 걱정할 필요가 없게 됐다.

함수형 프로그래밍은 코드의 다른 부분을 재사용하는 추상적인 방법으로 함수를 생성하는 것이다.

순수 함수

순수 함수(pure function) 란 주어진 입력에 대해 동일한 출력을 반환하는 함수다.

1
var double = value => value * 2;

double 함수는 주어진 입력에 대해 항상 동일한 출력을 반환하므로 순수 함수다. 순수 함수는 참조 투명성을 만족한다. 따라서 망설임 없이 double(5) 를 10으로 바꿀 수 있다.

순수 함수는 테스트하기 편한 코드다.

순수하지 않은 함수는 문제를 일으킬 수 있다.

1
2
3
// 세금 계산 함수
var percentValue = 5;
var calculateTax = value => value / 100 * (100 + percentValue);

외부 환경에 의존해 논리를 계산하므로, calculateTax 함수는 순수하지 않은 함수다. 함수는 동작하지만 테스트하기는 어렵다.
percentValue 에 의존하기 때문에 이 값이 변경되면 테스트를 통과할 수 없게 될 수 있다.

1
var calculateTax = (value, percentValue) => value / 100 * (100 + percentValue);

위의 함수처럼 외부 환경 종속성을 제거해서 이 문제를 해결할 수 있다.

순수 함수는 외부 환경 변수를 바꿔서는 안 된다. 순수 함수는 외부 변수에 의존해서는 안 되고 외부 변수도 변경해서는 안된다.

1
2
3
4
5
var global = "globalValue";
var badFunction = value => {
global = "changed";
return value * 2;
}

badFunction 함수가 전역 변수 global 값을 changed 로 바꿔서 호출하면 문제가 생길 수 있다. global 변수에 의존하는
다른 함수가 있다고 하면, badFunction 호출은 다른 함수의 동작에 영향을 줄 수 있다. 이러한 함수는 코드 테스트를 힘들게 한다.
테스트 외에도 시스템에 영향을 주어 디버깅을 어렵게 한다.

이상적인 코드

순수 함수를 사용하면 이상적인 코드를 간단히 만들 수 있다.

1
var double = value => value * 2;

함수 이름에서 알 수 있듯이 주어진 수를 두 배로 하는 함수다. 사실상 참조 투명성 개념을 사용해 double 함수 호출을 동일한 결과로 대체할 수 있다.
코드상 부수 효과가 있는 함수라면 다른 개발자가 이해하기 어렵다. 순수 함수 기반 코드는 읽고, 이해하고 테스트하기 쉽다.
순수 함수든 어떤 함수든 항상 의미 있는 이름이어야 한다.

병렬 코드

순수 함수는 병렬로 코드를 실행할 수 있게 한다. 순수 함수는 해당 환경을 전혀 변동시키지 않으므로 동기화를 걱정할 필요가 없다.

1
2
3
4
5
6
7
8
9
let global = "something";
let function1 = input => {
global = "somethingFalse";
}
let function2 = () => {
if (global === "something") {
// ...
}
}

function1 과 function2 를 병렬로 실행하게 된다면 두 함수 모두 전역 변수인 global 에 의존하므로, 병렬로 이 함수를 실행하면
문제가 발생할 수 있다.

1
2
3
4
5
6
7
8
let function1 = (input, global) => {
global = "somethingFalse";
}
let function2 = global => {
if (global === "something") {
// ...
}
}

두 함수의 global 변수를 인자로 옮겨 순수 함수로 만들었다. 이제 아무 문제 없이 두 함수를 동시에 실행할 수 있다.

캐시

순수 함수는 항상 주어지니 입력에 대해 동일한 출력을 반환하므로, 함수 출력을 캐시할 수 있다. 시간이 많이 소요되는 연산을 처리하는
함수가 있다고 해보자.

1
2
3
var longRunningFunction = ip => {

}

다음과 같이 longRunningFunction 함수의 호출 결과를 모두 유지하는 저장 객체를 갖고 있다고 가정해보자.

1
var longRunningFnBookKeeper = {2: 3, 4: 5 ...}

longRunningFnBookKeeper 는 간단한 자바스크립트 객체로, longRunningFunction 함수를 호출해 입력과 출력 값을 모두 보유하게 된다.
이제 기존 함수를 실행하기 전에 longRunningFnBookKeeper 에 키가 있는지 확인할 수 있다.

1
2
var longRunningFnBookKeeper = {2: 3, 4: 5};
longRunningFnBookKeeper.hasOwnProperty(ip) ? longRunningFnBookKeeper[ip] : longRunningFnBookKeeper[ip] = longRunningFunction(ip);

실제 함수를 호출하기 전에 해당 ip 와 함께 함수의 결과가 저장 객체에 있는지 확인한다. 함수 결과가 저장 객체에 있다면 반환하고, 그렇지 않으면 기존 함수를 호출해 저장 객체에서도
결과를 갱신한다. 적은 코드로도 쉽게 캐시할 수 있는 함수 호출을 만들 수 있다는 것을 살펴봤다. 이것이 바로 순수 함수의 역할이다.

참조: 함수형 자바스크립트 입문 2/e

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.