And Brain said,
함수적인, 너무나 함수적인 - 6. 함수형 프로그래밍의 고급 기법 본문
Index
0. 함수형 프로그래밍
1. 람다 대수 (Lambda Calculus)와 합성함수
2. 범주론 (Category Theory)
3. 모노이드 (Monoide)
4. 펑터 (Functor)와 엔도펑터(Endo Functor)
5. 모나드 (Monad)
6 함수형 프로그래밍의 고급 기법
이 챕터에서는 함수형 프로그래밍의 고급 기법들에 대해 간략하게 소개만 하고 넘어갈 것입니다. 이 시리즈의 핵심은 이전 챕터의 모나드임을 강조드리는 바이니 참고 바랍니다. 그렇다고 각각의 내용들이 함수형 프로그래밍에 있어서 중요하지 않은 내용들이 아닙니다. 오늘 소개한 것들은 꼭 더 공부해보시길 바랍니다.
부분 적용, 커링 및 메모이제이션 (Partial Application, Currying, and Memoization)
부분 적용 (Partial Application)은 함수가 받아야 하는 인자 중 일부를 고정시키고 나머지 인자들만 받아 결과를 반환하는 새로운 함수를 생성하는 기법입니다. 커링 (Currying)은 모든 함수가 오직 하나의 인자만을 받도록 변환하는 과정입니다. 메모이제이션 (Memoization)은 함수의 결과를 캐시하여 동일한 입력에 대해 여러 번 호출되는 경우 중복된 계산을 방지하는 기법입니다.
// 부분 적용: 함수가 받아야 하는 인자 중 일부를 고정시키고, 나머지 인자들만 받아 결과를 반환하는 새로운 함수를 생성합니다.
// 여기서 add 함수는 두 개의 인자를 받아 더하는 함수입니다.
function add(a: number, b: number): number {
return a + b;
}
// partialApply는 func 함수의 첫 번째 인자를 고정시키고, 새로운 함수를 반환합니다.
function partialApply(func: (a: number, b: number) => number, a: number): (b: number) => number {
return (b: number) => func(a, b);
}
const add5 = partialApply(add, 5); // add5는 첫 번째 인자가 5로 고정된 새로운 함수입니다.
console.log(add5(3)); // 8, 첫 번째 인자 5와 두 번째 인자 3을 더한 결과입니다.
// 커링: 모든 함수가 오직 하나의 인자만을 받도록 변환하는 과정입니다.
// curry는 두 개의 인자를 받는 함수를 하나의 인자를 받는 함수로 변환합니다.
function curry(func: (a: number, b: number) => number): (a: number) => (b: number) => number {
return (a: number) => (b: number) => func(a, b);
}
const curriedAdd = curry(add); // curriedAdd는 add 함수를 커링한 함수입니다.
const add10 = curriedAdd(10); // add10은 첫 번째 인자가 10으로 고정된 새로운 함수입니다.
console.log(add10(4)); // 14, 첫 번째 인자 10와 두 번째 인자 4를 더한 결과입니다.
// 메모이제이션: 함수의 결과를 캐시하여 동일한 입력에 대해 여러 번 호출되는 경우 중복된 계산을 방지하는 기법입니다.
// memoize는 함수를 받아 결과를 캐시하는 함수를 반환합니다.
function memoize(func: (n: number) => number): (n: number) => number {
const cache: Record<number, number> = {};
return (n: number) => {
if (cache[n] === undefined) {
cache[n] = func(n);
}
return cache[n];
};
}
function slowFactorial(n: number): number {
if (n === 0) return 1;
return n * slowFactorial(n - 1);
}
const memoizedFactorial = memoize(slowFactorial); // memoizedFactorial은 결과를 캐시하는 팩토리얼 함수입니다.
console.log(memoizedFactorial(5)); // 120, 첫 번째 호출에서 계산한 결과입니다.
console.log(memoizedFactorial(5)); // 120, 캐시에서 가져온 결과로 중복 계산을 방지합니다.
지연 평가 (Lazy Evaluation)
지연 평가는 값이 필요할 때까지 계산을 미루는 기법입니다. 이를 통해 무한한 데이터 구조를 다루거나 불필요한 계산을 피할 수 있습니다.
// 지연 평가: 함수의 결과가 필요할 때까지 실행을 미루고, 최종적으로 필요한 결과만 계산하는 기법입니다.
// 값을 담는 간단한 Lazy 클래스를 만들어봅시다.
class Lazy<T> {
private _value: T | null = null;
constructor(private readonly _fn: () => T) {}
// 값을 얻기 위해 실행하고, 결과를 캐시하여 이후 호출에서 재사용합니다.
public getValue(): T {
if (this._value === null) {
this._value = this._fn();
}
return this._value;
}
}
// 일반적인 함수로 두 수를 곱하는 함수를 정의합니다.
function multiply(a: number, b: number): number {
console.log(`Calculating ${a} * ${b}`);
return a * b;
}
// 지연 평가를 사용하지 않은 경우, multiply 함수는 즉시 실행됩니다.
const result1 = multiply(2, 3);
console.log(`Result 1: ${result1}`); // 출력: "Calculating 2 * 3" 후 "Result 1: 6"
// Lazy 클래스를 사용하여 지연 평가를 적용한 함수를 생성합니다.
const lazyMultiply = new Lazy(() => multiply(4, 5));
console.log("Lazy multiply created."); // 출력: "Lazy multiply created."
// getValue()를 호출할 때만 계산이 실행됩니다.
const result2 = lazyMultiply.getValue();
console.log(`Result 2: ${result2}`); // 출력: "Calculating 4 * 5" 후 "Result 2: 20"
패턴 매칭 (Pattern Matching)
패턴 매칭은 데이터 구조를 분해하고 검사하여 특정한 구조와 일치하는 경우에 코드를 실행하는 기법입니다. 이를 통해 코드를 더 명확하고 간결하게 작성할 수 있습니다.
// 패턴 매칭: 데이터의 구조를 기반으로 한 일치 여부를 확인하고, 일치하는 경우에만 실행하는 코드 블록을 선택하는 기법입니다.
// Shape 타입과 여러 도형 타입을 정의합니다.
type Shape = Circle | Rectangle | Triangle;
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
// 패턴 매칭을 이용한 도형의 면적을 계산하는 함수를 정의합니다.
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // 원의 면적: 파이 * 반지름^2
case "rectangle":
return shape.width * shape.height; // 사각형의 면적: 가로 * 세로
case "triangle":
return 0.5 * shape.base * shape.height; // 삼각형의 면적: 1/2 * 밑변 * 높이
default:
throw new Error("Unknown shape");
}
}
// 각각의 도형 객체를 생성합니다.
const circle: Circle = { kind: "circle", radius: 5 };
const rectangle: Rectangle = { kind: "rectangle", width: 4, height: 6 };
const triangle: Triangle = { kind: "triangle", base: 3, height: 4 };
// 패턴 매칭을 사용하여 각 도형의 면적을 계산합니다.
console.log("Circle area:", area(circle)); // 출력: "Circle area: 78.53981633974483"
console.log("Rectangle area:", area(rectangle)); // 출력: "Rectangle area: 24"
console.log("Triangle area:", area(triangle)); // 출력: "Triangle area: 6"
리커시브 함수와 꼬리 재귀 최적화 (Recursive Functions and Tail-Call Optimization)
리커시브 함수는 자기 자신을 호출하는 함수입니다. 꼬리 재귀 최적화는 리커시브 함수의 마지막 호출을 최적화하여 스택 오버플로를 방지하는 기법입니다.
// 리커시브 함수: 자기 자신을 호출하는 함수입니다.
// 꼬리 재귀 최적화: 꼬리 호출 형태의 재귀 호출을 반복문으로 바꾸어 스택 공간을 절약하는 최적화 기법입니다.
// 팩토리얼 함수를 리커시브 함수로 구현합니다.
function factorial(n: number): number {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
}
console.log("Factorial (recursive):", factorial(5)); // 출력: "Factorial (recursive): 120"
// 팩토리얼 함수를 꼬리 재귀 최적화를 적용하여 구현합니다.
function factorialTailRecursive(n: number, accumulator: number = 1): number {
if (n === 0) {
return accumulator;
}
return factorialTailRecursive(n - 1, n * accumulator);
}
console.log("Factorial (tail recursive):", factorialTailRecursive(5)); // 출력: "Factorial (tail recursive): 120"
// 주의: TypeScript 및 JavaScript는 기본적으로 꼬리 재귀 최적화를 지원하지 않습니다.
// ECMAScript 2015 (ES6)에서는 TCO (Tail Call Optimization)를 도입하였으나,
// 대부분의 브라우저 환경에서 구현 및 지원이 완벽하지 않습니다.
여기까지 함수형 프로그래밍의 중요한 고급 기법들에 대해 간단한 소개를 마치겠습니다.
끝으로,
아래는 사족입니다. 안 읽으셔도 되기에 접어둡니다.
이렇게해서 함수적인, 너무나 함수적인 시리즈가 막을 내렸습니다. 아직도 너무나 부족하고 배울 것은 항상 더 많습니다만은 모나드까지 가는 과정에 도통 난해한 개념들이 실타래처럼 묶여있었지만, 결국 하나의 목적지에서 교차했다 다시 그 개념들이 확장되는 느낌을 많이 받았습니다.
이 시리즈를 연재하기위해, 올해 들어 가장 열심히 공부했던 것 같습니다. 도서관에 가서 유지니아 쳉(범주론을 전공한 수학자 겸 피아니스트 겸 작가)의 범주론 관련 서적도 뒤져가며, 수학적 사고를 함양하기 위해 노력했던 것 같습니다. 저는 수학을 제일 못했습니다. 어렸을 때부터 지금까지 제 발목을 잡은 것은 수학이었고, 제가 공학 공부를 포기한 것도 수학이었습니다. 다시 돌아보면, 그렇게 어려운 공부도 아니었음에도 말이지요. 최근에 이런 말을 들은 적이 있습니다. (수없이 들어온 진부한 얘기지만) "네가 하기 싫어하는 것이 네게 필요한 것이다." 그래서 사실 이 시리즈의 공부에 더 열을 올렸습니다. 제가 인생에서 가장 싫어했던 것은 명백하게 수학이었으니까요. 겨우 이정도 공부하고 이런 말을 하기도 웃기지만, 하기싫고 두려운 일을 피하지 않겠단 마음가짐으로 이를 악물고 전혀 이해가 안되는 수학으로 뛰어들었습니다.
말을 좀 두서없이 했지만, 수학적 사고가 지금 당장은 필요하지 않더라도 이 시리즈를 연재한 것이 제게 더 앞으로 나아갈 원동력이 될 것 같습니다. 지금의 저는 시리즈가 끝났으니 후련함도 있지만, 빨리 다른 공부를 시작하고 싶다는 생각이 더 큽니다.
여하튼간에 앞으로 더 좋은 글들을 쓸 수 있도록 계속해서 노력하겠습니다. 그럼,
As always, Thanks for watching, Have a nice day.
References
https://velog.io/@kwonh/ES6-%EA%B3%A0%EC%B0%A8%ED%95%A8%EC%88%98-%EC%BB%A4%EB%A7%81-%EB%B6%80%EB%B6%84%EC%A0%81%EC%9A%A9%ED%95%A8%EC%88%98
https://armadillo-dev.github.io/javascript/whit-is-lazy-evaluation/
https://wondytyahng.tistory.com/entry/memoization-%EB%A9%94%EB%AA%A8%EC%9D%B4%EC%A0%9C%EC%9D%B4%EC%85%98
https://seokba.tistory.com/14
https://madplay.github.io/post/time-complexity-space-complexity
https://onlyfor-me-blog.tistory.com/548
https://velog.io/@dldhk97/%EC%9E%AC%EA%B7%80%ED%95%A8%EC%88%98%EC%99%80-%EA%BC%AC%EB%A6%AC-%EC%9E%AC%EA%B7%80
'IT > 함수적인, 너무나 함수적인' 카테고리의 다른 글
함수적인, 너무나 함수적인 - 5. 모나드(Monad) (4) | 2023.05.01 |
---|---|
함수적인, 너무나 함수적인 - 4. 펑터 (Functor)와 엔도펑터(Endo Functor) (4) | 2023.04.22 |
함수적인, 너무나 함수적인 - 3. 모노이드(Monoid) (0) | 2023.04.17 |
함수적인, 너무나 함수적인 - 2. 범주론 (Category Theory) (0) | 2023.04.16 |
함수적인, 너무나 함수적인 - 1. 람다 대수 (Lambda Calculus)와 합성 함수 (0) | 2023.04.16 |