Million Dreams
100만개의 꿈을 꾸는 개발자 지망생
Do it! 타입스크립트 프로그래밍 8장

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

 

- 프로그래밍 언어로 수학 함수를 구현할 때는 변수 xy의 타입을 고려해야함.

(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)))

 

- 함수형 프로그래밍에서는 composepipe라는 이름의 함수를 사용해 compose(h, g, f) 또는 pipe(f, g, h)형태로 f, g, h 함수들을 조합해 새로운 함수를 만들 수 있음

 

 

(1) 고차 함수란?

- 어떤 함수가 또 다른 함수를 반환할 때 그 함수를 고차 함수(high-orderfunction)이라고 함

 

- 단순히 값을 반환하는 함수를 1차 함수라고 하면, 1차 함수를 반환하면 2차 고차함수, 2차 함수를 반환하면 3차 고차 함수라고 표현함

 

1차 함수의 예

import {FirstOrderFuncfrom './function-signature'

 

export const inc: FirstOrderFunc<numbernumber= (x: number): number => x + 1



 

2차 고차 함수의 예

import {FirstOrderFuncSecondOrderFuncfrom './function-signature'

 

export const add: SecondOrderFunc<numbernumber=

    (x: number): FirstOrderFunc<numbernumber=>

    (y: number): number => x + y

 

2차 고차 함수의 사용 예

import {addfrom './second-order-func'

console.log(

    add(1)(2)

)

 

결괏값: 3

 

- 2차 고차 함수를 호출할때는 add(1)(2) 처럼 호출 연산자를 두번 연속해서 사용하며 이를 함수 프로그래밍 언어에서는 커리(curry)라고 함.

 

3차 고차 함수의 예

import {FirstOrderFuncSecondOrderFuncThirdOrderFuncfrom './function-signature'

 

export const add3: ThirdOrderFunc<numbernumber=

    (x: number): SecondOrderFunc<numbernumber=>

    (y: number): FirstOrderFunc<numbernumber=>

    (z: number): number => x + y + z

 

(2) 부분 적용 함수와 커리

- 고차 함수들은 자신의 차수만큼 함수 호출 연산자를 연달아 사용함 (add(1)(2) )

 

- 이때, 자신의 차수보다 함수 호출 연산자를 덜 사용하면 부분 적용 함수(partially applied function), 짧게 말하면 부분 함수(partial function)라고함

 

부분함수 적용 예

import {FirstOrderFuncSecondOrderFuncfrom './function-signature'

import {addfrom './second-order-func'

 

const add1: FirstOrderFunc<numbernumber= add(1)

console.log(

    add1(2),

    add(1)(2)

)

 

- add11차 함수 이므로 add1(2) 처럼 함수 호출 연산자를 1개 호출해 일반 함수처럼 호출할 수 있으며, add1add(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) // result3을 저장한 후 변수 X 메모리 해제

- 이처럼 고차 함수가 부분 함수가 아닌 을 발생해야 비로소 자유 변수의 메모리가 해제되는 유효 범위를 클로저라고 함.

 

- 클로저는 메모리가 해제되지 않고 프로그램이 끝날 때까지 지속될 수도 있다.

 

8-4) 함수 조합

- 함수 조합(function composition)은 작은 기능을 구현한 함수를 여러 번 조합해 더 의미 있는 함수를 만들어 내는 프로그램 기반 설계 기법이며, compose 또는 pipe라는 이름의 함수를 제공하거나 만들 수 있다.

 

(1) compose 함수

- compose 함수는 가변 인수 스타일로 함수들의 배열을 입력받으며, 매개변수 x를 입력받는 1차 함수를 반환함.

export const compose = <TR>(...functions: readonly Function[]): Function =>

(x: T): (T) => R => {

    const deepCopiedFunctions = [...functions]

    return deepCopiedFunctions.reverse().reduce((valuefunc) => func(value), x)

}

 

 

compose 함수와 에리티 1인 함수 f, g, h함수의 조합

import {fghfrom './f-g-h'

import {composefrom './compose'

 

const composedFGH = compose(hgf)

console.log(

    composedFGH('x')

)

 

결괏값 : h(g(f(x)))

 

(2) pipe 함수

- pipe 함수는 compose와 매개변수들을 해석하는 순서가 반대이므로, compose와는 달리 functionsreverse 하는 코드가 없음

export const pipe = <TR>(...functions: readonly Function[]): Function => (x:T):

(T) => R => {

    return functions.reduce((valuefunc) => func(value), x)

}

 

pipe 함수 실행 예

import {fghfrom './f-g-h'

import {pipefrom './pipe'

 

const piped = pipe(fgh)

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 배열의 마지막 아이템인 hreduce메서드의 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 {pipefrom './pipe'

 

const add = x => y => x + y

const inc = add(1)

 

const add3 = pipe(

    inc,

    add(2)

)

 

console.log(

    add3(1)

)

 

- 위 소스에서 add2차 고차 함수이므로 inc함수는 add의 부분함수가 되며, add3 pipe함수를 가지고 incadd(2) 두 부분 함수를 조합해 만든 함수임

 

(5) 포인트가 없는 함수

- map(f) 형태의 부분 함수를 만들면 composepipe에서 사용할 수 있으며, 이처럼 함수를 조합을 고려한 함수를 포인트가 없는 함수(pointless function)’라고 함

 

 

 

 

 

  Comments,     Trackbacks