3장: 순수 함수로 순수한 행복을
아, 다시 순수해지는 것
우리가 분명히 해야 할 한 가지는 순수 함수의 개념입니다.
순수 함수란 동일한 입력을 받으면 항상 동일한 출력을 반환하고, 관찰 가능한 부작용이 없는 함수입니다.
slice와 splice를 예로 들어보겠습니다. 이 두 함수는 본질적으로 같은 작업을 하지만, 매우 다른 방식으로 수행합니다. slice는 매번 동일한 입력에 대해 동일한 출력을 반환하기 때문에 순수합니다. 반면 splice는 배열을 변경하여 항상 변화를 일으키므로 관찰 가능한 효과가 있습니다.
var xs = [1, 2, 3, 4, 5];
// 순수
xs.slice(0, 3);
//=> [1, 2, 3]
xs.slice(0, 3);
//=> [1, 2, 3]
xs.slice(0, 3);
//=> [1, 2, 3]
// 비순수
xs.splice(0, 3);
//=> [1, 2, 3]
xs.splice(0, 3);
//=> [4, 5]
xs.splice(0, 3);
//=> []
함수형 프로그래밍에서는 splice와 같은 데이터 변형 함수는 선호하지 않습니다. 우리는 항상 동일한 결과를 반환하는 신뢰할 수 있는 함수를 추구하기 때문에 splice와 같은 함수는 적합하지 않습니다.
다른 예를 보겠습니다.
// 비순수
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 순수
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
비순수 코드에서는 checkAge가 결과를 결정하기 위해 변경 가능한 변수 minimum에 의존합니다. 즉, 시스템 상태에 의존하므로 외부 환경을 도입하여 인지 부하를 증가시킵니다.
이 예제에서는 큰 문제가 아닌 것처럼 보이지만, 상태 의존성은 시스템 복잡성을 증가시키는 주요 원인 중 하나입니다 . 이 checkAge 함수는 입력 외부의 요인에 따라 다른 결과를 반환할 수 있으며, 이는 순수하지 않을 뿐만 아니라 소프트웨어를 이해하는 데 부담을 줍니다.
순수한 형태에서는 완전히 독립적입니다. minimum을 변경할 수 없도록 만들어 순수성을 유지할 수도 있습니다. 이를 위해 객체를 생성하고 고정해야 합니다.
var immutableState = Object.freeze({
minimum: 21,
});
SideEffect...
"SideEffect"을 더 살펴보고 직관을 개선해 봅시다. 순수 함수 정의에서 언급된 분명히 불길한 부작용이란 무엇일까요? 우리는 효과를 결과 계산 외에 발생하는 모든 것으로 정의할 것입니다.
효과 자체는 본질적으로 나쁘지 않으며, 다음 장에서 곳곳에서 사용할 것입니다. 문제는 SideEffect가 부정적인 의미를 가진다는 점입니다. 물은 자체로는 애벌레를 키우는 것이 아니지만, 정체된 물이 그렇듯이, 부작용도 프로그램에서 비슷한 문제를 일으킵니다.
SideEffect란 결과를 계산하는 동안 발생하는 시스템 상태의 변화 또는 외부 세계와의 관찰 가능한 상호작용입니다.
SideEffect의 예는 다음과 같습니다.
- 파일 시스템 변경
- 데이터베이스에 레코드 삽입
- HTTP 호출
- 데이터 변형
- 화면 출력 / 로깅
- 사용자 입력 받기
- DOM 쿼리
- 시스템 상태 접근
이 목록은 계속 이어집니다. 함수 외부와의 모든 상호작용은 부작용입니다. 이를 통해 부작용 없이 프로그래밍하는 것이 실용적이지 않을 수 있다고 의심할 수 있습니다. 함수형 프로그래밍 철학은 부작용이 잘못된 동작의 주요 원인이라고 가정합니다.
부작용을 사용하지 말라는 것이 아니라, 이를 제어된 방식으로 실행하고자 합니다. 나중 장에서 펑터와 모나드를 통해 이를 배우겠지만, 지금은 이러한 부정적인 함수를 순수한 함수와 분리해 보겠습니다.
부작용은 함수를 순수하지 않게 합니다. 이는 순수 함수가 동일한 입력에 대해 항상 동일한 출력을 반환해야 한다는 정의와 일치하지 않기 때문입니다.
왜 동일한 입력에 대해 동일한 출력을 고집하는지 자세히 살펴보겠습니다. 중학교 수학 수준으로 돌아가 봅시다.
중학교 수학
mathisfun.com에 따르면:
함수란 값들 사이의 특별한 관계입니다: 각 입력값은 정확히 하나의 출력값을 반환합니다.
즉, 함수는 입력과 출력 사이의 관계일 뿐입니다. 각 입력은 정확히 하나의 출력을 가지지만, 그 출력이 반드시 고유할 필요는 없습니다. 왼편은 x에서 y로의 완벽하게 유효한 함수의 다이어그램입니다;
대조적으로, 오른편의 다이어그램은 입력값 5가 여러 출력을 가리키기 때문에 함수가 아닌 관계를 보여줍니다:
함수는 입력과 출력의 쌍 집합으로 설명될 수 있습니다: [(1,2), (3,6), (5,10)] (이 함수는 입력을 두 배로 하는 것처럼 보입니다).
또는 표로 표현할 수 있습니다:
Input | Output |
1 | 2 |
2 | 4 |
3 | 6 |
또는 x를 입력으로, y를 출력으로 하는 그래프로 표현할 수도 있습니다:
입력이 출력을 결정하면 구현 세부 사항은 필요하지 않습니다. 함수는 입력에서 출력으로의 매핑일 뿐이므로, 객체 리터럴을 작성하고 () 대신 []로 실행할 수 있습니다.
var toLowerCase = {
'A': 'a',
'B': 'b',
'C': 'c',
'D': 'd',
'E': 'e',
'F': 'f',
};
toLowerCase['C'];
//=> 'c'
var isPrime = {
1: false,
2: true,
3: true,
4: false,
5: true,
6: false,
};
isPrime[3];
//=> true
물론, 직접 계산하고 싶을 수도 있지만, 이는 함수를 다른 방식으로 생각하는 것을 보여줍니다. (여러 인수를 가진 함수는 어떨까요? 물론, 이는 수학적으로 생각할 때 불편함을 줍니다. 지금은 배열로 묶거나 arguments 객체를 입력으로 생각할 수 있습니다. 커링을 배우면 수학적 함수 정의를 직접 모델링할 수 있습니다.)
여기서 중요한 점은: 순수 함수는 수학적 함수이며, 이는 함수형 프로그래밍의 핵심입니다. 이러한 작은 천사들로 프로그래밍하는 것은 큰 이점을 제공할 수 있습니다. 순수성을 유지하기 위해 많은 노력을 기울이는 이유를 살펴보겠습니다.
순수성을 지키는 이유
캐시 가능
먼저, 순수 함수는 입력에 따라 항상 캐시될 수 있습니다. 이는 일반적으로 메모이제이션이라는 기법을 사용하여 수행됩니다:
var squareNumber = memoize(function(x) {
return x * x;
});
squareNumber(4);
//=> 16
squareNumber(4); // 입력 4에 대한 캐시 반환
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 입력 5에 대한 캐시 반환
//=> 25
다음은 간단한 구현이지만, 더 강력한 버전도 많이 있습니다.
var memoize = function(f) {
var cache = {};
return function() {
var arg_str = JSON.stringify(arguments);
cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
return cache[arg_str];
};
};
어떤 비순수 함수도 평가를 지연시켜 순수하게 만들 수 있습니다:
var pureHttpCall = memoize(function(url, params) {
return function() {
return $.getJSON(url, params);
};
});
흥미로운 점은 실제 HTTP 호출을 하는 대신, 호출 시 특정 HTTP 호출을 할 함수를 반환한다는 것입니다. 이 함수는 동일한 입력에 대해 항상 동일한 출력을 반환하기 때문에 순수합니다: url과 params를 주어진 HTTP 호출을 수행할 함수입니다.
memoize 함수는 잘 작동하며, HTTP 호출의 결과를 캐시하지 않고, 생성된 함수를 캐시합니다.
이제까지는 크게 유용하지 않을 수 있지만, 곧 더 유용한 방법을 배울 것입니다. 중요한 점은 모든 함수를 순수하게 만들어 캐시할 수 있다는 것입니다.
이식 가능 / 자기 문서화
순수 함수는 완전히 독립적입니다. 함수가 필요로 하는 모든 것이 명시적으로 전달됩니다. 이는 어떻게 유리할까요? 우선, 함수의 의존성이 명시적이므로 더 쉽게 이해할 수 있습니다.
// 비순수
var signUp = function(attrs) {
var user = saveUser(attrs);
welcomeUser(user);
};
var saveUser = function(attrs) {
var user = Db.save(attrs);
...
};
var welcomeUser = function(user) {
Email(user, ...);
...
};
// 순수
var signUp = function(Db, Email, attrs) {
return function() {
var user = saveUser(Db, attrs);
welcomeUser(Email, user);
};
};
var saveUser = function(Db, attrs) {
...
};
var welcomeUser = function(Email, user) {
...
};
순수 함수는 의존성을 명확히 하며, 함수 시그니처만으로도 필요한 것들을 알 수 있습니다. 이는 프로그램을 이해하는 데 큰 도움이 됩니다.
순수 형태에서는 "의존성 주입"을 통해 데이터베이스나 메일 클라이언트를 인자로 전달하므로 더 유연한 애플리케이션을 만들 수 있습니다. 다른 데이터베이스를 사용하고 싶다면 함수 호출 시 이를 전달하면 됩니다.
자바스크립트 환경에서는 이식성이 함수 직렬화 및 소켓을 통한 전송을 의미할 수 있습니다. 이는 웹 워커에서 모든 애플리케이션 코드를 실행할 수도 있음을 의미합니다. 이식성은 강력한 특성입니다.
순수 함수는 공유 메모리에 접근할 필요가 없고, 부작용으로 인한 경쟁 상태가 없으므로 병렬로 실행할 수 있습니다.
요약
순수 함수가 무엇인지, 왜 함수형 프로그래머들이 순수함수를 중요하게 여기는지 살펴보았습니다. 앞으로 모든 함수를 순수하게 작성하려고 노력할 것입니다. 이를 위해 몇 가지 도구가 필요하지만, 그동안 순수한 코드와 그렇지 않은 코드를 분리해 보겠습니다.
순수 함수로 프로그래밍하는 것은 몇 가지 추가 도구 없이 다소 힘들 수 있습니다. 데이터를 인자로 전달해야 하고, 상태와 효과를 사용할 수 없습니다. 이러한 프로그래밍을 어떻게 할 수 있을까요? 다음 장에서는 커리를 배우며 이 문제를 해결해 보겠습니다.
'Functional Programming' 카테고리의 다른 글
mostly-adequate-guide-02 (1) | 2024.07.16 |
---|---|
mostly-adequate-guide-01 (2) | 2024.07.16 |