Chap08 함수 조합의 원리와 응용
8-1) 함수형 프로그래밍이란?
- 함수형 프로그래밍은 순수 함수와 선언형 프로그래밍의 토대 위에 함수 조합(function composition)과 모나드 조합(monadic composition)으로 코드를 설계하고 구현하는 기법이다.
함수형 프로그래밍은 다음 세 가지 수학 이론에 기반을 두고 있다.
1. 람다 수학(ramda calculus): 조합 논리와 카테고리 이론의 토대가 되는 논리 수학
2. 조합 논리(combinatory logic): 함수 조합의 이론적 배경
3. 카테고리 이론(category theory): 모나드 조합과 고차 타입의 이론적 배경
- 함수형 프로그래밍 언어는 한때 인공지능의 언어로 불렸던 LISP에 기반을 두고 있다. LISP는 메타 언어(meta language)로 진화되었고, 메타언어는 다시 하스켈(Haskell) 언어로 발전되었음
- 하스켈 언어는 스칼라 언어에 의해 C언어 와 비슷한 구문을 갖게 되었으며, 타입스크립트는 스칼라 언어 구문을 자바스크립트 친화적으로 발전했음
- 타입스크립트는 함수형 언어에서 중요한 패턴 매칭과 고차 타입이라는 기능을 생략해 구문을 쉽게 만듦
8-2) 제네릭 함수
(1) 타입스크립트의 제네릭 함수 구문
- 타입스크립트에서 제네릭 타입은 함수와 인터페이스, 클래스, 타입 별칭에 적용할 수 있으며, 꺾쇠 괄호 <> 타입을 감싼 <T>, <T, Q>처럼 표현함.
Function g1<T>(a: T): void {}
Function g2<T, Q>(a: T, b: Q): void {}
- 제네릭 타입으로 함수를 정의하면 어떤 타입에도 대응이 가능.
※ 화살표 함수에 제네릭 타입을 적용한 예
Const g3 = <T>(a: T): void => {}
Const g4 = <T, Q>(a: T, b: Q): void => {}
※ 타입 별칭(type-alias)에 제네릭 타입을 적용한 예
type Type1Func<T> = (T) => void
type Type2Func<T, Q> = (T, Q) => void
type Type3Func<T, Q, R> = (T, Q) => R
(2) 함수의 역할
- 수학에서 함수는 값 x에 수식을 적용해 또 다른 y 값을 만드는 역할을 하며, 함수를 f라 표기하면 다음과 같이 관계를 표현할 수 있음
x ~> f ~> y
- 프로그래밍 언어로 수학 함수를 구현할 때는 변수 x와 y의 타입을 고려해야함.
(x: T) ~-> f -> (y: R)
- 수학에서는 이런 관계를 일대일 관계(one-to-one relationship)라고 하고, 이런 동작을 하는 함수 f를 매핑(mapping) 줄여서 맵(map)이라고 표현함. 타입스크립트로 일대일 맵 함수를 만든다면 타입 T인 값을 이용해 타입 R인 값을 만들어 주어야 하므로, 다음과 같이 표현 가능함
Type MapfFunc<T, R> = (T) => R
(3) 아이덴티티 함수
- 맵 함수의 가장 단순한 형태는 입력값(x)을 그대로 반환하는 것으로, 입력과 출력 타입이 같은 형태
- 함수형 프로그래밍에서는 identitiy 또는 I라는 단어가 포함됨.
※ Identity 함수의 예
Const numberIdentity: IdentitiyFunc<number> = (x: number): number => x
Const stringIdentity: IdentityFunc<string> = (x: string): string => x
Const objectIdentity: IdentityFunc<object> = <x: object): object => x
Const arrayIdentity: IdentityFunc<any[]> = (x: any[]): any[] => x
8-3) 고차 함수와 커리
- 함수에서 매개변수의 개수를 애리티(arity)라고 한다. F()은 애리티가 0인 함수, f(X)는 애리티가 1인 함수, f(x, y)는 애리티가 2인 함수임
- 만약 함수 f, g, h의 애리티가 모두 1이라면 다음과 같이 연결해서 사용가능
x ~> f ~> g ~> h ~> y
- 이를 프로그래밍 언어로 표현하면 다음과 같음
y = h(g(f(x)))
- 함수형 프로그래밍에서는 compose나 pipe라는 이름의 함수를 사용해 compose(h, g, f) 또는 pipe(f, g, h)형태로 f, g, h 함수들을 조합해 새로운 함수를 만들 수 있음
(1) 고차 함수란?
- 어떤 함수가 또 다른 함수를 반환할 때 그 함수를 고차 함수(high-orderfunction)이라고 함
- 단순히 값을 반환하는 함수를 1차 함수라고 하면, 1차 함수를 반환하면 2차 고차함수, 2차 함수를 반환하면 3차 고차 함수라고 표현함
※ 1차 함수의 예
import {FirstOrderFunc} from './function-signature'
export const inc: FirstOrderFunc<number, number> = (x: number): number => x + 1
※ 2차 고차 함수의 예
import {FirstOrderFunc, SecondOrderFunc} from './function-signature'
export const add: SecondOrderFunc<number, number> =
(x: number): FirstOrderFunc<number, number> =>
(y: number): number => x + y
※ 2차 고차 함수의 사용 예
import {add} from './second-order-func'
console.log(
add(1)(2)
)
결괏값: 3
- 위 2차 고차 함수를 호출할때는 add(1)(2) 처럼 호출 연산자를 두번 연속해서 사용하며 이를 함수 프로그래밍 언어에서는 커리(curry)라고 함.
※ 3차 고차 함수의 예
import {FirstOrderFunc, SecondOrderFunc, ThirdOrderFunc} from './function-signature'
export const add3: ThirdOrderFunc<number, number> =
(x: number): SecondOrderFunc<number, number> =>
(y: number): FirstOrderFunc<number, number> =>
(z: number): number => x + y + z
(2) 부분 적용 함수와 커리
- 고차 함수들은 자신의 차수만큼 함수 호출 연산자를 연달아 사용함 (add(1)(2) 등)
- 이때, 자신의 차수보다 함수 호출 연산자를 덜 사용하면 부분 적용 함수(partially applied function), 짧게 말하면 부분 함수(partial function)라고함
※ 부분함수 적용 예
import {FirstOrderFunc, SecondOrderFunc} from './function-signature'
import {add} from './second-order-func'
const add1: FirstOrderFunc<number, number> = add(1)
console.log(
add1(2),
add(1)(2)
)
- add1은 1차 함수 이므로 add1(2) 처럼 함수 호출 연산자를 1개 호출해 일반 함수처럼 호출할 수 있으며, add1은 add(1)과 같으므로 add1을 add(1)로 대체하면 add(1)(2)가 됨.
(3) 클로저
- 고차 함수의 몸통에서 선언되는 변수들은 클로저(closure)라는 유효범위를 가지며, 클로저는 ‘지속되는 유효 범위(persistence scope)’를 의미
※ 클로저를 사용한 함수 예
Function add(x: number): (number) => number {
Return function(y: number): number {
Return x + y
}
}
- 위 소스에서 add가 반환하는 함수의 내부 범위(inner scope)만 놓고 볼 때 x는 이해할 수 없는 변수이며 범위 안에서 그 의미를 알 수 없는 변수를 ‘자유 변수(free variable)’라고 함
- 클로저를 지속하는 유효 볌위라고 하는 이유는 다음처럼 add 함수를 호출하더라도 변수 x가 메모리에서 해제되지 않음.
Const add1 = add(1) // 변수 X 메모리 유지
Const result = add1(2) // result에 3을 저장한 후 변수 X 메모리 해제
- 이처럼 고차 함수가 부분 함수가 아닌 ‘값’을 발생해야 비로소 자유 변수의 메모리가 해제되는 유효 범위를 ‘클로저’라고 함.
- 클로저는 메모리가 해제되지 않고 프로그램이 끝날 때까지 지속될 수도 있다.
8-4) 함수 조합
- 함수 조합(function composition)은 작은 기능을 구현한 함수를 여러 번 조합해 더 의미 있는 함수를 만들어 내는 프로그램 기반 설계 기법이며, compose 또는 pipe라는 이름의 함수를 제공하거나 만들 수 있다.
(1) compose 함수
- compose 함수는 가변 인수 스타일로 함수들의 배열을 입력받으며, 매개변수 x를 입력받는 1차 함수를 반환함.
export const compose = <T, R>(...functions: readonly Function[]): Function =>
(x: T): (T) => R => {
const deepCopiedFunctions = [...functions]
return deepCopiedFunctions.reverse().reduce((value, func) => func(value), x)
}
※ compose 함수와 에리티 1인 함수 f, g, h함수의 조합
import {f, g, h} from './f-g-h'
import {compose} from './compose'
const composedFGH = compose(h, g, f)
console.log(
composedFGH('x')
)
결괏값 : h(g(f(x)))
(2) pipe 함수
- pipe 함수는 compose와 매개변수들을 해석하는 순서가 반대이므로, compose와는 달리 functions을 reverse 하는 코드가 없음
export const pipe = <T, R>(...functions: readonly Function[]): Function => (x:T):
(T) => R => {
return functions.reduce((value, func) => func(value), x)
}
※ pipe 함수 실행 예
import {f, g, h} from './f-g-h'
import {pipe} from './pipe'
const piped = pipe(f, g, h)
console.log(
piped('x')
)
(3) pipe와 compose 함수 분석
- 먼저 pipe함수는 pipe(f), pipe(f, g), pipe(f, g, h)처럼 가변 인수 방식으로 동작하므로 매개변수를 다음처럼 설정
Export const pipe = (…functions)
- 가변인수 functions는 인자마다 함수 타입이 다를 수 있기 때문에 자바스크립트 타입 Function들의 배열인 Function[]으로 설정함.
Export const pipe = (…functions: Function[])
- pipe 함수는 functions 배열을 조합해 어떤 함수를 반환해야 하므로 반환 타입은 Function으로 설정
Expoert const pipe = (…functions: Function[]): Function
- pipe로 조합된 결과 함수는 애리티가 1이므로, 매개변수 x를 입력받는 함수를 작성하되, 제네릭 타입으로 표현하면 타입 T의 값 x를 입력받아 (T) => R타입의 함수를 반환하는 것이 됨
Export const pipe = <T, R>(…functions: Function[]): Function => (x: T) => (T) => R
- 몸통에서는 배열이 제공하는 reducㄷ 메서드를 사용해 변수 x를 reduce 메서드의 초깃값으로 설정함
Export const pipe = <T, R>(…functions: Function[]): Function => (x: T) => (T) => R => {
Return functions.reduce(<함수>, x)
}
- <함수> 부분은 (value, func) 형태의 매개변수 구조를 가져야 하는데, reduce 메서드의 두번째 매개변수(x)는 항상 배열의 아이템이기 때문
Export const pipe=<T, R>(…functions: Function[]): Function =>(x: T) => (T) => R => {
Return functions.reduce((value, func) => func(value), x)
}
- 위 코드를 기준으로 functions의 내용이 [f, g, h]라고 가정했으므로 reduce ptjem의 진행 순서별 매개변수값의 변화는 다음과 같음
순서 |
Value |
Func |
결괏값 |
1 |
x |
f |
f(x) |
2 |
f(x) |
g |
g(f(x)) |
3 |
g(f(x)) |
h |
h(g(f(x))) |
- functions 배열의 마지막 아이템인 h가 reduce메서드의 func 매개변수에 입력되면 최종결괏값은 h(g(f(x)))가 되고 해당 값을 반환함
- compose 함수는 pipe 함수와 매개변수 방향이 반대이므로, 즉 pipe(f, g, h)는 compose(h, g, f)와 같음
- 일반적으로 생각했을떄 functions.reverse()만 호출하면 나머지는 pipe와 똑같이 작성하면 될 것 같지만, compose는 순수 함수의 모습으로 동작해야한다. 따라서 functions을 전개 연산자로 전개하고 그 내용을 깊은 복사를 하는 변수(deepCopiedFunctions)를 만듦
Export const pipe=<T, R>(…functions: Function[]): Function =>(x: T) => (T) => R => {
Const deepCopiedFunctions = […functions]
deepCopiedFunctions.reverse().reduce((value, func) => func(value), x)
}
(4) 부분함수와 함수 조합
- 고차 함수의 부분 함수는 함수 조합에 사용될 수 있음
import {pipe} from './pipe'
const add = x => y => x + y
const inc = add(1)
const add3 = pipe(
inc,
add(2)
)
console.log(
add3(1)
)
- 위 소스에서 add는 2차 고차 함수이므로 inc함수는 add의 부분함수가 되며, add3은 pipe함수를 가지고 inc와 add(2) 두 부분 함수를 조합해 만든 함수임
(5) 포인트가 없는 함수
- map(f) 형태의 부분 함수를 만들면 compose나 pipe에서 사용할 수 있으며, 이처럼 함수를 조합을 고려한 함수를 ‘포인트가 없는 함수(pointless function)’라고 함
'TypeScript' 카테고리의 다른 글
Do it! 타입스크립트 프로그래밍 10장 (0) | 2020.05.21 |
---|---|
Do it! 타입스크립트 프로그래밍 9장 (0) | 2020.05.20 |
Do it! 타입스크립트 프로그래밍 7장 (0) | 2020.05.07 |
Do it! 타입스크립트 프로그래밍 6장 (0) | 2020.05.06 |
Do it! 타입스크립트 프로그래밍 5장 (0) | 2020.05.03 |