람다로 표현할 수 있는 합성 함수를 알아본다.
doctest
예시로 들 여러 함수에 대해 유닛 테스트를 수행할 수 있는 헤더만 존재하는 오픈 소스를 사용한다.
https://github.com/doctest/doctest
#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);
}
'Advanced C++' 카테고리의 다른 글
[C++] Memory Pool (1) | 2024.03.31 |
---|---|
[Functional C++] 함수형 프로그래밍 (3) 파셜 어플리케이션과 커링 (0) | 2022.10.08 |
[Functional C++] 함수형 프로그래밍 (1) 불변성과 순수 함수 (0) | 2022.10.03 |
[C++] Protobuf (Google Protocol Buffer) 라이브러리 (0) | 2022.09.27 |
[C++] Redis 사용하기 (0) | 2022.08.18 |