고차 함수와 커링
함수형 프로그래밍의 핵심적인 특징 중 하나는 함수를 일급 객체(First-Class Citizen) 로 취급한다는 점입니다. 이는 함수를 변수에 할당하고, 다른 함수의 인자로 전달하며, 함수의 반환 값으로 사용할 수 있음을 의미합니다. 이러한 일급 함수의 특성을 기반으로 고차 함수(Higher-Order Functions, HOF) 와 커링(Currying) 이라는 강력한 개념이 탄생합니다.
타입스크립트는 함수의 타입을 명확하게 정의할 수 있어, 고차 함수와 커링을 구현하고 이해하는 데 큰 도움을 줍니다.
고차 함수
정의: 고차 함수는 다음 중 하나 이상을 수행하는 함수입니다.
하나 이상의 함수를 인자로 받습니다.
함수를 결과로 반환합니다.
설명:
고차 함수는 함수를 데이터처럼 다룰 수 있게 해주어 코드의 추상화 수준을 높이고, 재사용성을 향상시키며, 중복을 줄이는 데 기여합니다. 자바스크립트/타입스크립트에서는 이미 많은 내장 고차 함수를 접했습니다. 예를 들어, 배열의 map
, filter
, reduce
메서드가 대표적인 고차 함수입니다. 이들은 콜백 함수(다른 함수)를 인자로 받아서 내부 로직을 수행합니다.
타입스크립트 적용 예시
함수를 인자로 받는 고차 함수
// 배열의 각 요소를 특정 함수에 따라 변환하는 고차 함수
function applyTransformation<T, U>(arr: T[], transformFn: (item: T) => U): U[] {
return arr.map(transformFn); // map 자체가 고차 함수이지만, applyTransformation도 HOF
}
const numbers = [1, 2, 3, 4];
// 숫자를 두 배로 만드는 함수
const double = (n: number): number => n * 2;
const doubledNumbers = applyTransformation(numbers, double);
console.log("Doubled Numbers:", doubledNumbers); // [2, 4, 6, 8]
// 숫자를 문자열로 변환하는 함수
const toString = (n: number): string => `Number: ${n}`;
const stringNumbers = applyTransformation(numbers, toString);
console.log("String Numbers:", stringNumbers); // ["Number: 1", "Number: 2", "Number: 3", "Number: 4"]
// 타입스크립트는 transformFn의 타입과 반환 타입을 정확히 추론하고 검사합니다.
applyTransformation
함수는 transformFn
이라는 또 다른 함수를 인자로 받습니다. 이를 통해 다양한 변환 로직을 유연하게 적용할 수 있습니다.
함수를 반환하는 고차 함수 (함수 팩토리)
// 특정 기준보다 큰 숫자를 필터링하는 함수를 반환하는 고차 함수
function createFilterByMin(minThreshold: number): (num: number) => boolean {
return (num: number): boolean => num > minThreshold;
}
const data = [10, 25, 5, 40, 15];
// 20보다 큰 숫자를 필터링하는 함수 생성
const filterGreaterThan20 = createFilterByMin(20);
const filteredData1 = data.filter(filterGreaterThan20);
console.log("Greater than 20:", filteredData1); // [25, 40]
// 30보다 큰 숫자를 필터링하는 함수 생성
const filterGreaterThan30 = createFilterByMin(30);
const filteredData2 = data.filter(filterGreaterThan30);
console.log("Greater than 30:", filteredData2); // [40]
// 타입스크립트는 createFilterByMin의 반환 타입이 (num: number) => boolean 임을 명확히 합니다.
createFilterByMin
함수는 minThreshold
값을 클로저(closure)로 포획하여, 새로운 필터링 함수를 동적으로 생성하여 반환합니다. 이는 특정 조건을 미리 설정해둔 함수를 재사용할 때 유용합니다.
고차 함수의 이점
- 코드 재사용성: 일반적인 로직을 고차 함수로 추상화하여 다양한 상황에서 재사용할 수 있습니다.
- 유연성 및 확장성: 로직을 외부 함수로 분리함으로써, 기존 코드를 수정하지 않고 새로운 동작을 쉽게 추가하거나 변경할 수 있습니다 (OCP 준수).
- 추상화: 복잡한 세부 구현을 숨기고, 코드의 의도를 더 명확하게 표현할 수 있습니다.
- 결합도 감소: 특정 로직이 다른 코드와 강하게 묶이지 않고 독립적으로 존재할 수 있습니다.
커링
정의: 커링은 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수들의 체인으로 변환하는 함수형 프로그래밍 기법입니다.
설명: 커링된 함수를 호출하면 첫 번째 인자를 받고, 이어서 두 번째 인자를 받을 함수를 반환하고, 또 다음 인자를 받을 함수를 반환하는 식으로 동작합니다. 모든 인자가 채워지면 비로소 최종 결과가 반환됩니다.
기존 함수: f(a, b, c)
커링된 함수: f(a)(b)(c)
커링은 람다 대수(Lambda Calculus)에서 유래했으며, 특정 함수형 언어에서는 기본적으로 지원됩니다. 자바스크립트/타입스크립트에서는 직접 구현하거나 라이브러리(Ramda
, Lodash/fp
등)의 도움을 받아 사용할 수 있습니다.
타입스크립트 구현 예시
// 일반적인 3개 인자를 받는 함수
function add(a: number, b: number, c: number): number {
return a + b + c;
}
console.log("Normal add:", add(1, 2, 3)); // 6
// 커링된 add 함수 (직접 구현)
function curriedAdd(a: number): (b: number) => (c: number) => number {
return (b: number) => {
return (c: number) => {
return a + b + c;
};
};
}
// 모든 인자를 한 번에 전달 (일반 함수처럼 사용)
console.log("Curried add (all at once):", curriedAdd(1)(2)(3)); // 6
// 2. 부분 적용 (Partial Application) - 커링의 핵심 장점
const add5 = curriedAdd(5); // 첫 번째 인자 5를 고정하여 새로운 함수 생성
console.log("add5(1)(2):", add5(1)(2)); // 5 + 1 + 2 = 8
const add5And10 = add5(10); // 첫 번째, 두 번째 인자를 고정하여 또 다른 함수 생성
console.log("add5And10(3):", add5And10(3)); // 5 + 10 + 3 = 18
// 3. 타입스크립트의 타입 추론
// TypeScript는 curriedAdd 함수의 복잡한 타입을 정확히 추론합니다.
// (a: number) => (b: number) => (c: number) => number
curriedAdd
함수는 첫 번째 인자 a
를 받은 후, b
를 받을 함수를 반환하고, 이 함수가 b
를 받은 후 c
를 받을 함수를 반환합니다. 최종적으로 c
를 받으면 모든 인자가 채워져 덧셈 결과가 반환됩니다.
커링의 이점
- 부분 적용 (Partial Application): 커링은 함수에 인자를 부분적으로 적용하여 새로운 함수를 생성하는 것을 자연스럽게 만듭니다. 이는 특정 인자를 고정하여 재사용 가능한 특수화된 함수를 만들 때 매우 유용합니다.
- 함수 조합 용이: 작은 단위의 커링된 함수들을 조합하여 더 복잡한 기능을 쉽게 구축할 수 있습니다. 각 함수의 입력과 출력이 명확해져 파이프라인 구성이 쉬워집니다.
- 재사용성 증가: 미리 인자를 고정하여 새로운 함수를 생성할 수 있으므로, 코드의 재사용성이 높아집니다.
- 가독성: 적절히 사용하면 함수 호출 체인이 데이터 흐름을 명확하게 보여주어 가독성을 높일 수 있습니다.
- 지연 실행: 모든 인자가 채워질 때까지 실제 계산을 지연시킬 수 있습니다.
고차 함수와 커링의 관계
커링된 함수는 그 자체로 고차 함수의 한 형태입니다. 커링된 함수는 인자를 하나 받은 후 함수를 반환하기 때문입니다. 따라서 커링은 고차 함수를 활용하여 함수를 더 유연하고 재사용 가능하게 만드는 특정 패턴이라고 볼 수 있습니다.
실제 예시 (Logger with Currying)
type LogLevel = 'INFO' | 'WARN' | 'ERROR';
// 커링된 로거 생성 함수
function createLogger(level: LogLevel): (tag: string) => (message: string) => void {
return (tag: string) => {
return (message: string) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] [${tag}] ${message}`);
};
};
}
// 특정 로그 레벨의 로거 함수 생성 (부분 적용)
const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');
// 특정 태그의 로거 함수 생성 (또 다른 부분 적용)
const userLogger = infoLogger('USER');
const authLogger = errorLogger('AUTH');
// 메시지 로깅
userLogger('New user registered!');
userLogger('User login successful.');
authLogger('Failed login attempt for user: john_doe');
authLogger('Session expired for admin user.');
// 직접 커링된 함수 사용
createLogger('WARN')('NETWORK')('Network connection lost!');
이 예시에서 createLogger
는 커링된 고차 함수입니다. 이를 통해 infoLogger
, errorLogger
, userLogger
, authLogger
와 같이 특정 목적에 맞게 인자가 부분적으로 적용된 특화된 로거 함수들을 쉽게 생성하고 재사용할 수 있습니다. 이는 로깅 시스템의 유연성과 구성 가능성을 크게 높여줍니다.
고차 함수와 커링은 함수형 프로그래밍의 강력한 도구로, 코드의 추상화, 재사용성, 유연성 및 테스트 용이성을 향상시키는 데 크게 기여합니다. 타입스크립트의 정적 타입 검사는 이러한 복잡한 함수 타입을 다룰 때 안정성을 보장하며, 개발자가 더욱 자신감을 가지고 함수형 패턴을 적용할 수 있도록 돕습니다.