Advanced C++

[Functional C++] 함수형 프로그래밍 (2) 합성 함수

로파이 2022. 10. 4. 23:59

람다로 표현할 수 있는 합성 함수를 알아본다.

 

doctest

예시로 들 여러 함수에 대해 유닛 테스트를 수행할 수 있는 헤더만 존재하는 오픈 소스를 사용한다.

https://github.com/doctest/doctest

 

GitHub - doctest/doctest: The fastest feature-rich C++11/14/17/20 single-header testing framework

The fastest feature-rich C++11/14/17/20 single-header testing framework - GitHub - doctest/doctest: The fastest feature-rich C++11/14/17/20 single-header testing framework

github.com

#define DOCTEST_CONFIG_IMPLEMENT
#include <doctest/doctest.h>

// 사용할 테스트 코드가 포함된 헤더
#include "functional.h"

int main(int argc, char** argv) {
    doctest::Context context;

    // defaults
    context.addFilter("test-case-exclude", "*math*"); // exclude test cases with "math" in the name
    context.setOption("rand-seed", 324);              // if order-by is set to "rand" use this seed
    context.setOption("order-by", "file");            // sort the test cases by file and line

    context.applyCommandLine(argc, argv);

    // overrides
    context.setOption("no-breaks", true); // don't break in the debugger when assertions fail

    int res = context.run(); // run queries, or run tests unless --no-run is specified

    if (context.shouldExit()) // important - query flags (and --exit) rely on the user doing this
        return res;          // propagate the result of the tests

    context.clearFilters(); // removes all filters added up to this point

    return 0;
}

위 main 함수에 포함될 functional.h 헤더에는 자신이 원하는 테스트 코드를 작성한다.

 

다음은 가장 간단한 increment 에 대한 테스트 코드이다.

TEST_SUITE("functional-compositions") {
	auto increment = [](const int value) { return value + 1; };

	TEST_CASE("Increments Value") {
		CHECK_EQ(2, increment(1));
	}
	
    // ...
}
  • TEST_SUITE : 작성할 테스트 모듈을 모아두는 네임 스페이스 역할을한다.
  • TEST_CASE : 실제 테스트를 수행할 스코프를 정의한다.

두 함수를 두 번 연속하여 사용하는 함수를 작성할 수 있다.

	TEST_CASE("Increments twice") {
		CHECK_EQ(3, increment(increment(1)));
	}

	// lambda version
	auto incrementTwiceLambda = [](int value) {
		return increment(increment(value));
	};

 	// square of int
	auto square = [](int value) { return value * value; };

	auto incrementSquareLambda = [](int value) {
		return increment(square(value));
	};

	TEST_CASE("Increments the squared number"){
		CHECK_EQ(5, incrementSquareLambda(2));
	}

 

square를 먼저 수행하고 increment를 수행하는 incrementSquareLambda는 f = increment 그리고 g = square의 합성 함수로 표현 할 수 있다.

C(x) = f(g(x))

 

 이를 function<int(int)>로 나타내면 다음과 같다.

	std::function<int(int)> compose(function<int(int)> f, function<int(int)> g) {
		return [f, g](auto x) { return f(g(x)); };
	}

 

템플릿을 사용하여 좀 더 일반화한 경우는 다음과 같으며 합성하다compose라고 할 수 있다. 

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

auto 타입의 매개변수로 받는 람다를 반환하는 것이 특징이다.

 

이제 compose로 두 함수의 합성 함수를 표현 가능해졌다.

	TEST_CASE("Increments square with composed lambda") {
		auto incrementSquare = compose(increment, square);
		CHECK_EQ(5, incrementSquare(2));
	}

 

다수의 인자에 대한 합성 함수

만약, 둘 이상의 매개변수를 사용하는 함수를 합성하려면 어떻게 해야할까. 다음과 같이 작성해야할 지도 모른다.

template<class F1, class G2>
auto compose12(F1 f, G2 g) {
    return [=](auto first, auto second) { return f(g(first, second)); };
}

위 함수는 피함수 g가 두 개의 매개변수를 사용하고 f는 하나의 매개변수를 사용한다.

반대로 합성 피함수 g는 하나의 매개변수만을 사용할 수도 있다. 그렇다면 다음과 같은 변형이 필요하다.

template<class F2, class G1>
auto compose21(F2 f, G1 g) {
    return [=](auto first, auto second) { return f(g(first), g(second)); };
}

 

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

두 개의 매개변수를 갖는 multiply 함수를 하나의 매개변수를 갖는 함성 함수로 분해한다.

auto multiply = [](const int first, const int second) {
    return first * second;
};

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

TEST_CASE("Adds using single paraeter functions") {
    CHECK_EQ(4, multiplyDecomposed(2)(2));
}

여기서 특이한 것은 첫번째 람다에서는 첫 인수만 받고 두번째 람다에서 첫 인수를 캡처하여 사용한다는 것이다.

이제 multiply(2,2) 가 아니라 multiply(2)(2)로 호출가능해졌다.

 

두 인자를 받는 함수를 하나의 인자를 갖는 두 개의 함수로 분해하는 것을 일반화한다면 다음과 같다.

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

TEST_CASE("Multiplies using single parameter functions") {
    CHECK_EQ(4, decomposeToOneParameter(multiply)(2)(2));
}

 

두 수를 곱한 다음 증가시키는 함수

 

compose를 사용해서 두 수의 곱에 하나를 증가시키는 함수를 만들어본다.

먼저 h = f * g합성하기 위해 g를 두 수의 곱에 대한 함수 f를 하나를 증가시키는 함수로 만든다.

	auto increment = [](const int value) { return value + 1; };
    
	auto multiplyDecomposed = [](const int first) {
		return [=](const int second) { return first * second; };
	};

	TEST_CASE("Increment result of multiplication") {
		int first = 2;
		int second = 2;
		auto incremnetResultOfMultiplication = compose(increment, multiplyDecomposed);
		CHECK_EQ(5, incremnetResultOfMultiplication(first)(second));
	}

위 코드는 컴파일 되지 않는데 increment는 정수를 입력값으로 받고 multiplyDecomposed는 람다 함수

([=](const int second) { return first* second;})를 반환하기 때문이다. f의 입력값으로 정수를 반환하는 g를 만들기 위해서는 multiplyDecomposed(first)로 first가 캡처되어 곱을 계산한 정수형 결과(first*second)를 반환하는 함수를 사용한다.

 

TEST_CASE("Increment result of multiplication") {
    int first = 2;
    int second = 2;
    auto incremnetResultOfMultiplication = compose(increment, multiplyDecomposed(first));
    CHECK_EQ(5, incremnetResultOfMultiplication(second));
}

compose에 두 인자로는 각각 f와 g로 합성가능한 함수가 와야한다. (g의 결과가 f의 입력으로 호환되어야한다.)

 

multiplyDecomposed를 일반화 시키면 어떠한 두 인자를 받는 함수를 분해하는 decomposeToOneParameter와 mulitply를 사용해서 나타낼 수 있다.

TEST_CASE("Increment result of multiplication") {
    auto incrementResultOfMultiplication = [](int first, int second) {
        return compose(increment, decomposeToOneParameter(multiply)(first))(second);
    };

    int result = incrementResultOfMultiplication(2, 2);
    CHECK_EQ(5, result);
}

 

두 수를 증가시킨 후 곱을 하는 함수

 

기본 람다를 사용하면 다음과 같이 작성 가능하다.

TEST_CASE("Multiply incremented values no compose") {
    auto multiplyIncrementedValues = [](int first, int second) {
        return multiply(increment(first), increment(second));
    };

    int result = multiplyIncrementedValues(2, 2);
    CHECK_EQ(9, result);
}

 

multiply(int a, int b) = multiplyDecomposed(int a)(int b)로 작성 가능하기 때문에 다음과 같이 나타낼 수 있다.

TEST_CASE("Multiply incremented values decompose") {
    auto multiplyIncrementedValues = [](int first, int second) {
        return multiplyDecomposed(increment(first))(increment(second));
    };
    int result = multiplyIncrementedValues(2, 2);
    CHECK_EQ(9, result);
}

 

compose를 사용해서 multiplyDecomposed = f 그리고 increment = g의 합성 함수로 나타낼 수 있다.

TEST_CASE("Multiply incremented values compose simple") {
    auto multiplyIncrementedValues = [](int first, int second) {
        return compose(multiplyDecomposed, increment)(first)(increment(second));
    };

    int result = multiplyIncrementedValues(2, 2);
    CHECK_EQ(9, result);
}

 

마지막으로 일반화시켜 multiplyDecomposed = decomposeToOneParameter(multiply)로 나타낼 수 있다.

TEST_CASE("Multiply incremented values decompose first") {
    auto multiplyIncrementedValues = [](int first, int second) {
        return compose(decomposeToOneParameter(multiply), increment)(first)(increment(second));
    };

    int result = multiplyIncrementedValues(2, 2);
    CHECK_EQ(9, result);
}

 

더 일반화 시키기

 

두 수의 곱에 하나 더하기

auto incrementResultOfMultiplication = [](int first, int second) {
    return compose(increment, decomposeToOneParameter(multiply)(first))(second);
};

increment와 multiply는 일반화 할 수 있으며 각 하나의 인자를 받는 함수 f(x)와 g(x,y)로 생각할 수 있다.

따라서 다음과 같이 템플릿을 이용하여 일반화가 가능하다.

template <class F, class G>
auto composeWithTwoParamteres(F f, G g) {
    return [=](auto first, auto second) {
        return compose(f, decomposeToOneParameter(g)(first))(second);
    };
}

 

두 수를 증가시킨 후 곱하기

auto multiplyIncreentedValues = [](int first, int second) {
    return compose(decomposeToOneParameter(multiply), increment)(first)(increment(second));
};

위 람다 함수에서 increment는 중복된다. 그리고 multiply 함수도 일반화가 가능하다.

template<class F, class G>
auto composeWithFunctionCallAllParameters(F f, G g) {
    return [=](auto first, auto second) {
        return compose(decomposeToOneParameter(f), g) (first)(g(second));
    };
};
TEST_CASE("Multiply incremented valeus generalized") {
    auto multiplyIncrementedValeus =
        composeWithFunctionCallAllParameters(multiply, increment);
    int result = multiplyIncreentedValues(2, 2);
    CHECK_EQ(9, result);
}