KoreanFoodie's Study

[C++ 함수형 프로그래밍] 함수형 합성 (C++ 에서 함수 합성하기) 본문

R & D/Software Engineering

[C++ 함수형 프로그래밍] 함수형 합성 (C++ 에서 함수 합성하기)

GoldGiver 2023. 9. 28. 00:43

함수형 프로그래밍 패러다임에 대해 알아보며 이를 C++ 를 이용한 소프트웨어 개발에 어떻게 적용하면 좋을지 알아보겠습니다.

핵심 :

1. 함수(람다)는 값이다. 고로 함수는 인자로 전달할 수 있고, 람다를 반환할 수 있으며 람다도 람다를 반환할 수 있다.

2. 순수 함수의 조합을 통한 설계는 불변성을 보장한다. 우리는 간단한 람다를 조합하여 고차원의 합성 함수를 만들어낼 수 있다.

3. 함수형 합성을 통해 중복을 효과적으로 제거할 수 있다. 또한 여러 인자를 가진 람다도 하나의 인자와 캡쳐된 값들을 가진 다수의 람다로 분해할 수 있다.

함수형 합성

우리는 고등학교 시간에 함수 합성에 대해 배웠다(혹은 중학교일수도 있다). 

놀랍게도, 우리는 C++ 에서 동일한 작업을 할 수 있다. 😛 우리는 이제 람다를 합성할 것이다.

template <typename F, typename G>
auto compose(F f, G g)
{
	return [=](auto value) { return f(g(value)); };
}

위 템플릿 함수를 한번 활용해 보자.

우리는 인자에 값을 1 더하고, 제곱을 하는 함수를 만들어 볼 것이다.

auto increment = [](auto value) {return value + 1; };
auto square = [](auto value) {return value * value; };

int value = 1;
auto incrementAndSquare = compose(square, increment);
incrementAndSquare(value);

cout << "Result : " << incrementAndSquare(value) << endl;

그리고 함수형 합성에는 교환 법칙이 성립하지 않는다는 것도 명심하자 😀

 

그런데 위의 예시는 인자를 1개만 받고 있다. 다수의 인자를 가진 함수를 조합하려면 어떻게 해야 할까?

만약 두 수를 각각 1씩 증가시키고 곱하거나, 곱하고 1씩 증가시키는 것을 원한다고 가정해 보자. 그럼 각 상황에 맞게, compose 를 약간 변형해야 한다. 일단 증가 및 곱셉 함수는 다음과 같을 것이다 :

auto increment = [](auto i) {return i + 1; };
auto multiply = [](auto i, auto j) {return i * j; };

그럼 합성을 위한 함수는 다음과 같이 작성할 수 있다 :

// 각각을 1씩 증가시키고, 곱하는 합성을 위한 Lambda
template <typename F, typename G>
auto compose12(F f, G g)
{
    return [=](auto i, auto j) {return g(f(i), f(j)); };
};

// 두 수를 곱하고 1을 증가시키는 합성을 위한 Lambda
template <typename F, typename G>
auto compose21(F f, G g)
{
    return [=](auto i, auto j) {return f(g(i, j)); };
};

실제로 예제를 돌려보면, 다음과 같은 결과를 확인할 수 있다 😉

int i = 1, j = 2;

// (1 + 1) * (2 + 1) = 6
auto incrementAndMultiply = compose12(increment, multiply);
cout << "Result : " << incrementAndMultiply(i, j) << endl;

// (1 * 2) + 1 = 3;
auto multiplyAndIncrement = compose21(increment, multiply);
cout << "Result : " << multiplyAndIncrement(i, j) << endl;

그런데 인자의 갯수에 따라 각 조합마다 compose 함수를  만드는 것은 매우 지치는 작업을 것이다. 다행히도, 람다를 이용하여 여러 인자를 가진 함수를 더 적은 인수를 받는 람다로 분해할 수 있다! 😊

 

 

다수의 인자를 가진 함수 분해

앞서 정의했던 multiply 함수를 단일 인자를 받는 람다 두 개로 한 번 쪼개보자. 다음과 같이 사용할 수 있을 것이다.

auto multiplyDecomposed = [](auto first) 
{
    return [=](auto second) {return first * second; };
};

위 변환을 좀 더 범용적으로 적용한다고 가정하면, 아래와 같은 템플릿 클래스가 탄생한다.

template <typename F>
auto decomposeToOneParam(F f)
{
    return [=](auto first)
    {
        return [=](auto second)
        {
            return f(first, second);
        };
    };
};

이 함수를 사용하면, 우리가 맨 처음에 정의했던 단일 인자를 받는 compose 함수를 활용할 수 있게 된다.

auto multiplyAndIncrement2 = [](int i, int j)
{
    return compose(increment, decomposeToOneParam(multiply)(i))(j);
};
cout << "Result : " << multiplyAndIncrement2(i, j) << endl;

이제 우리는 인자를 2개 받으면서 기존의 compose 를 활용하는 람다를 만든 것이다!

 

그렇다면 이번엔 반대로, 두 인자를 증가시킨 후 곱하고 싶다면 어떻게 해야 할까?

결론만 말하자면 이미 구현해 놓은 함수를 활용해 다음과 같이 표현할 수 있다! 😮

auto incrementAndMultiply2 = [](int i, int j)
{
    return compose(decomposeToOneParam(multiply), increment)(i)(increment(j));
};
cout << "Result : " << incrementAndMultiply2(i, j) << endl;

 

더 나아가서, 우리가 만든 함수를 좀 더 일반화 할 수 있다.

먼저 곱한 값에 1을 더하는 함수인 multiplyAndIncrement 를 일반화 해보면 아래와 같이 표현할 수 있다.

template <typename F, typename G>
auto ComposeWithTwoParams(F f, G g)
{
    return [=](int i, int j)
    {
        return compose(f, decomposeToOneParam(g)(i))(j);
    };
};

/* ... */

// (1 * 2) + 1 = 3;
auto multiplyAndIncrement = ComposeWithTwoParams(increment, multiply);
cout << "Result : " << multiplyAndIncrement(i, j) << endl;

위와 같이 표현할 수 있으며, 마찬가지로 각 수에 1을 더한 것을 곱하는 함수인 incrementAndMultiply 를 일반화 하면...

template <typename F, typename G>
auto ComposeWithFunctionCallTwoParams(F f, G g)
{
    return [=](int i, int j)
    {
        return compose(decomposeToOneParam(g), f)(i)(f(j));
    };
}

/* ... */

// (1 + 1) * (2 + 1) = 6
auto incrementAndMultiply = ComposeWithFunctionCallTwoParams(increment, multiply);
cout << "Result : " << incrementAndMultiply(i, j) << endl;

로 표현할 수 있다.

일단 이번 글에서 예제 분석은 여기서 마무리하고, 다음 글에서는 파셜 애플리케이션과 커링을 다루도록 하겠다!

Comments