함수형 프로그래밍에 대한 얕은 지식

함수형 프로그래밍에 대한 얕은 지식

최근에 타입스크립트와 함수형 프로그래밍을 주제로된 책의 베타 리더로 활동하면서 관련된 책 한 권을 읽었다. 책은 그야말로 JS, TS, Functional Programming에 대한 내용들이었는데 그 중에 함수형 프로그래밍은 이번 기회에 처음 접하게 되어서, 책을 통해 새로 배우는 함수형 프로그래밍에 대한 내용들은 간단하게 정리를 해두려고 한다.

배경지식: 프로그래밍 패러다임

프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해주고, 결정하는 역할을 한다.

선언형 프로그래밍과 명령형 프로그래밍

선언형 프로그래밍은 프로그램이 어떤 방법으로 해야 하는지를 나타내기보다 무엇인지를 설명하는 경우 “선언형”이라고 한다. 가장 대표적인 예시로는 웹페이지의 레이아웃이 있다. 제목, 본문, 메뉴 등 “무엇”을 나타내려고 하는지를 설명하는 것이기 때문이다.

명령형 프로그래밍은 반대로 “어떻게”를 명시하는 프로그램이다. 프로그래머가 알고리즘을 명시하고 목표를 명시하지 않게 프로그래밍을 한다면 이는 명령형 프로그래밍을 한 것이라고 볼 수 있다.

절차 지향, 객체 지향, 함수형 프로그래밍

  • 절차 지향 프로그래밍: 순서대로 명령을 받아 문제를 해결하는 방식이다. 가장 직관적이고 가장 오래된 방식이다.

  • 객체 지향 프로그래밍: 프로그램을 작게 나눠 각각의 단순한 기능을 모아서 문제를 해결하는 방식이다. 프로그램의 기능이 다양할 수록 코드는 복잡해진다. 절차 지향 프로그래밍에서는 이를 해결하기 위한 적절한 방법이 없었고 이러한 문제를 해결하기 위해 객체 지향 프로그래밍이라는 방법론이 제안 되었다. 단순한 문제를 해결하는 여러 기능으로 나누어 문제를 해결하는 것이다.

  • 함수형 프로그래밍: 프로그램을 하나의 큰 함수로 보고, 그것을 작은 함수들의 합성 함수로 구현해 문제를 해결하는 방식이다. 객체 지향 프로그래밍보다 더 문제를 세분화 하는 것이라고 볼 수 있는데,

함수형 프로그래밍과 용어들

함수형 프로그래밍은 순수 함수와 선언형 프로그래밍 토대 위에 함수 조합과 모나드 조합으로 코드를 설계하고 구현하는 기법이다. 용어들에 대해서 간단하게 정리해보자.

일급 객체, 일급 함수

일급 객체는 아래 조건을 만족하는 객체를 뜻한다.

  • 변수나 데이터 구조안에 담을 수 있다.
  • 인자값으로 전달 할 수 있다.
  • 반환값으로 사용할 수 있다.
  • 할당에 사용된 이름과 관계 없이 고유한 구별이 가능하다.
  • 동적으로 프로퍼티 할당이 가능하다.

자바스크립트의 객체는 일급 객체를 만족하고, 자바스크립트의 함수는 객체이므로 자바스크립트의 함수는 일급 함수이다. 어떤 경우엔 일급 함수를 위한 추가적인 조건도 요구하는데 그 추가적인 조건은 아래와 같다.

  • 런타임 생성이 가능하다.
  • 익명 생성이 가능하다.

마찬가지로 자바스크립트 함수에게 모두 해당되는 내용이므로 문제 없이 자바스크립트는 일급 함수 특성을 갖는다고 말할 수 있다.

고차 함수와 부분 함수

어떤 함수가 또 다른 함수를 반환할 때 그 함수는 고차 함수 (high order function)이라고 한다.

1
2
3
4

const add = (x:number): (number => number) => (y: number): number => x + y;

add(1)(2) // 3

위에서 구현된 add 함수는 2차 함수에 해당한다. 또한 호출 연산자를 2번 연속으로 사용하고 있는데 이를 함수형 프로그래밍에서는 커리(curry)라고 한다. 그런데 만약 add의 차수인 2보다 호출 연산자를 조금 사용하면, 즉 1번만 사용한 경우를 보자.

1
2
const add5 = add(5);
add5(10); // 15

위 예시에서 사용된 add5 함수는 부분 함수 - partial function (부분 적용 함수 - partially applied function)이라고 한다.

순수 함수

함수형 프로그래밍에서 함수는 순수 함수(pure function) 조건을 만족할 수 있어야 한다. 순수 함수는 부수 효과(side effect)가 없는 함수를 뜻한다. 부수 효과는 함수가 가진 목적 외 다른 효과를 갖는 것을 의미한다. 부수 효과를 가진 함수는 불순 함수(impure function)이라고 한다. 순수 함수는 다음 조건을 만족해야 한다.

  • 함수 내부에서 전역 변수, 정적 변수를 사용하지 않는다.
  • 함수가 예외를 발생시키지 않는다.
  • 비동기 방식으로 동작하는 코드가 없다.
  • 함수 body에서 만들어진 결과를 즉시 반환한다.
  • 함수 body에 매개 변수를 변경하지 않는다.
  • 함수 body에 입출력이 없어야 한다.

함수 조합

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

compose 함수

compose는 합성 함수를 의미한다. compose(f, g, h)는 수학적으로 f ∘ g ∘ h를 나타낸 것이다. 합성 함수의 기호를 떠올려보면 아래와 같이 작동하길 바라는 것이 수학적으로 잘 표현한 경우이다.

1
2
3
4
5
6
7
8
const f = <T>(val: T): string => `f(${val})`;
const g = <T>(val: T): string => `g(${val})`;
const h = <T>(val: T): string => `h(${val})`;

const composedFunc = compose(f, g, h);
composedFunc("some value");

// f(g(h(some value)))

타입스크립트에서 위와 같은 compose를 만들어보면 아래와 같을 것이다. 함수가 적용되는 순서에 유의해서 reverse 하는 과정을 넣어 줘야 한다.

1
2
3
4
5
6
const compose = <T, R>(...functionList: readonly Function[]): Function => (
x: T
): R => {
const deepCopiedList = [...functionList];
return deepCopiedList.reverse().reduce((val, func) => func(val), x);
};

pipe 함수

composepipe의 차이는 함수 적용되는 순서이다. compose는 의미상 먼저 인자 값으로 들어간 함수를 더 나중에 적용하게 된다. 하지만 pipe는 인자 값으로 들어간 순서대로 함수를 실행하길 원한다. 이러한 점을 고려해보면 위 compose 함수에서 reverse만 제외해주면 된다.

1
2
3
4
5
6
const pipe = <T, R>(...functionList: readonly Function[]): Function => (
x: T
): R => functionList.reduce((val, func) => func(val), x);

const pipedFunc = pipe(f, g, h);
pipedFunc("some value"); // h(g(f(some value)))

포인트가 없는 함수

포인트가 없는 함수(pointless function)라는 것은 함수 조합을 고려해 설계한 함수를 뜻한다. 예를 들어서 위 pipe 함수의 인자로 들어갈 함수들의 배열 (functionList)을 고려한 인자로 사용되는 함수를 아래와 같이 만들 수 있다.

1
2
3
4
5
6
7
8
const map = f => arr => arr.map(f);
const squareMap = map(val => val * val);

//const squareMap2 = arr => map(val => val * val)(arr);

const squarePipedFunc = pipe(squareMap, squareMap);

squarePipedFunc([5, 6]); // [(5 * 5) * (5 * 5), (6 * 6) (6 * 6) ]

위 코드에서 squareMap는 포인트가 없는 함수에 해당하고 squareMap2는 포인트가 있는 함수인 것이다. 완전히 이해하기는 어렵지만, 느낌적으로는 함수를 완성 시키는 게 아니고, 인자값으로 쓸 수 있는 정도로만 정의하고 함수의 인자로 전달하는 느낌이다. arr.filter(val => Boolean(val))형태가 아니라, arr.filter(Boolean) 같은 코드 느낌이 난다.

모나드

모나드는 카테고리 이론에서 사용되는 용어이다. 프로그래밍에서 모나드는 코드 설계 패턴으로서 몇 개의 인터페이스를 구현한 클래스이다. 모나드 클래스는 다음 4가지의 조건을 만족시켜야 한다.

  • Functor: map이라는 인스턴스 메서드를 가지는 클래스
  • Apply: Functor 이면서 ap라는 인스턴스 메서드를 가지는 클래스
  • Applicative: Apply면서 of라는 클래스 메서드를 갖는 클래스
  • Chain: Applicative면서 chain이라는 메서드를 가지는 클래스

타입 클래스와 고차 타입

1
2
3
4
5
6
7
8
9
10
class Monad<T> {
constructor(public value: T) {}
static of<U>(value: U): Monad<U> {
return new Monad<U>(value);
}

map<U>(fn: (x: T) => U): Monad<U> {
return new Monad<U>(fn(this.value));
}
}

위와 같은 Monad<T> 클래스를 타입 클래스라고 한다. 타입 클래스는 함수를 만들 때 특별한 타입으로 제약할 필요가 없다. 예를 들어서 아래 함수는 인자 값으로 들어오는 함수의 인자 값(b)가 map이라는 메소드를 가지고 있어야 한다. 따라서 타입스크립트로 작성할 때 map 함수를 가지고 있는 매개 변수임을 정의 해줘야 오류로부터 안전해진다.

1
2
const callMap = <T, U>(fn: (T) => U) => <T extends { map(fn) }>(b: T) =>
b.map(fn);

위와 같은 작업을 타입 클래스로 대체 하면 아래와 같다.

1
const callMonad = fn => b => Monad.of(b).map(fn).value;

위와 같이 타입에 따른 오류를 없애고, 코드 재사용성이 뛰어난 함수를 만들 수 있다.

1
2
callMonad((a: number) => a + 1)(1); // 2
callMonad((a: number[]) => a.map(val => val + 1))([1, 2, 3]); // [2, 3, 4]

모나드 클래스에서 타입 T를 잠시 Monad<T> 타입으로 바꾼 다음 T 타입이 필요해질 때 그 값을 주는 것을 확인할 수 있는데 이 때 Monad<T>를 고차 타입이라고 한다. 이 고차 타입에 대한 아이디어는 카테고리 이론에서 얻었다.

모나드 룰

어떤 클래스의 이름이 M, 인스턴스를 m이라고 했을 때, 모나드는 Applicative와 Chain 기능을 가지고 있고, 아래 두 법칙을 만족하게 구현한 클래스라고 볼 수 있다.

  • 왼쪽 법칙(left identity): M.of(a).chain(f) == f(a)
  • 오른쪽 법칙(right identity): m.chain(M.of) == m

마치며

함수형 프로그래밍에 대해서 처음 접하는 개념이 많아서 깔끔하게 정리가 안됐다. 아마 점차적으로 공부 하면서 더 나은 정리를 해볼 수 있지 않을까 싶다.

Reference

관련 글

댓글

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×