KoreanFoodie's Study
[C++ 함수형 프로그래밍] 순수 함수와 람다 본문
함수형 프로그래밍 패러다임에 대해 알아보며, 이를 C++ 를 이용한 소프트웨어 개발에 어떻게 적용하면 좋을지 알아보겠습니다.
핵심 :
1. 함수는 값이다. 함수는 인자로 전달할 수 있다. 간단한 함수를 조합해 더 크고 복잡한 함수를 만들 수 있다.
2. 순수 함수의 조합을 통한 설계는 불변성을 보장한다.
3. 람다를 순수 함수처럼 사용하자. 또한 캡쳐에 대해 명확히 알아두자.
사실 필자도 학부 과정 때 프로그래밍 언어 관련 수업을 들으며 함수형 언어를 접했지만, 그 당시에는 프로그래밍 자체에 대한 흥미가 그다지 없어서 그런지 큰 관심을 갖지 않았다. 열심히 하지도 않았고. 😅 그러다 보니 함수형 언어에 대한 지식은 머리 속 어느 구석에 앉아 먼지만을 수집하고 있었다. 그러다 다시 이 책을 읽으면서 함수형 프로그래밍이 매우 흥미롭다는 것을 느꼈고, 얼마나 유용한지에 대해 알게 되었다.
부디 포스팅을 읽는 분들이 비슷한 재미와 통찰을 느낄 수 있길 바라며... 나름의 리뷰를 남긴다. (관심이 생긴다면 책을 한 번 읽어볼 것을 권합니다 😉)
함수형 프로그래밍 소개
함수형 프로그래밍을 사용하면 중복을 제거하고 코드를 단순화할 수 있으며, 디자인을 단순화시킬 수 있다.
말만 들으면 잘 와닿지 않을 수 있다. 그래서 간단한 예시를 보면, C++ 에서 복잡한 루프를 통해 작업을 하던 것을 단순한 함수의 조합으로 표현할 수 있다.
예를 들어, 어떤 벡터에 들어 있는 정수에 대해, 짝수, 홀수, 모든 수를 각각 더한 3 개의 값을 결과값으로 반환하고 싶다고 하자.
만약 기존 명령형 방식을 차용한다면, 아마도 for loop 과 if 문을 늘려나가며 이를 해결할 것이다. 하지만 함수형으로 프로그래밍하면 다음과 같이 만들 수도 있다.
Sums theTotals
{
sum(filter(numbers, isEven)),
sum(filter(numbers, isEven)),
sum(numbers)
}
Sums 는 결과가 담길 클래스(혹은 구조체)이며, sum 은 합산하는 함수, filter 는 Predicate 를 받아 filtered 된 결과를 반환한다. 그리고 sum 은 목록을 합산해주는 함수이다. 세세한 구현은 직접 해 보도록 하자 😅
순수 함수 이해하기
C++ 에서 함수는 기본적으로 상태의 변화를 수반한다. 이를 방지하는 방법은 함수에 '눈치껏' const 를 붙여주는 것이다.
Effective C++ 에서 스콧 마이어 선생님이 '낌새만 보이면 const 를 들이대 보자!'고 조언하신 것처럼, 생각이 있는 프로그래머가 짠 코드에서는 심심치 않게 const 를 자주 찾아볼 수 있다.
const 를 밥 먹듯이 붙이는 이유는 최적화도 때문인 것도 있지만, 상태를 잘못 변화시키는 실수를 줄임으로써 유지보수에 있어 유리하게 대응할 수 있기 때문이다.
그런데 함수형 프로그래밍의 빌딩 블록은 순수 함수이며, 순수 함수를 활용하면 상태 변화에 의한 혼동이 줄어든다. 순수 함수는 다음 두 가지 제약 조건을 가진 함수를 의미한다.
- 동일한 인자 값을 넣으면 항상 동일한 결과 값을 반환한다.
- 입력 파라미터 값을 변경하지 않는다.
이러한 특징 덕에, 순수 함수는 부작용이 없다.
함수형 프로그래밍은 순수 함수를 조합해 가며 이러한 불변성을 매우 잘 지키는 방식으로 프로그램을 작성하겠다는 것을 의미한다. 순수 함수가 상태 변화를 일으키지 않는 특징 덕분에, 다형성과 모듈 확장성이라는 장점을 최대한으로 활용할 수 있는데, 이는 다른 함수에 함수를 전달할 때 잘 드러난다. 사실 STL 의 고차원 함수를 보면 이러한 특징을 살펴볼 수 있다.
C++ 에서 순수 함수의 불변성을 따라하려면 약간의 복잡한 사고 과정을 거쳐야 한다.
함수가 값에 의한 전달을 하는지, 참조에 의한 전달을 하는지, 전달 인자가 포인터인지 아닌지에 따라 const 를 이곳 저곳에 붙여야 한다. 예를 들어 다음과 같다 :
// 값에 의한 전달 클래스 함수
int increment(const int value) const;
// 참조에 의한 전달 클래스 함수
int increment(const int& value) const;
// 값에 의한 포인터 전달 클래스 함수
const int* increment(const int* value) const;
// 참조에 의한 전달 클래스 함수
const int* increment(const int* const& value) const;
이것 말고도, 예시는 더 쓸 수 있다. 참고로, 포인터는 기본적으로 '값에 의한 복사' 가 이루어진다는 것을 명심하자.
람다 파헤치기
람다는 변수와 값에 적용할 수 있는 순수 함수일 뿐이다. 잘 알고 있겠지만.. 예시를 보자.
auto add = [](int first, int second) { return first + second; };
캡쳐를 활용하면, 다음과 같이 쓸 수도 있다.
int first = 0;
auto add2 = [first](int second) {return first + second; };
auto add3 = [&first](int second) {return first + second; };
포인터도 캡쳐할 수 있다.
int* firstPtr = new int(5);
auto add4 = [firstPtr](int second) {return *firstPtr + second; };
auto add5 = [&firstPtr](int second) {return *firstPtr + second; };
일단 람다는 기본적으로 기본 값 한정자 [=] 로 캡쳐하는 것이 좋다. 참고로 포인터는 기본적으로 복사된다.
하지만 람다가 참조에 의해 전달된 인자를 받게 되면, 순수 함수의 기본적인 원칙을 위배하게 될 수도 있다. 왜냐하면 순수 함수는 파라미터의 값을 변경하면 안되기 때문이다. 이를 방지하기 위해서는 인자에 const 를 다시 도배해 주어야 한다.
인자 뿐만이 아니라 캡쳐도 마찬가지다. 예시를 보자.
int value = 1;
// value 가 2가 된다
auto increment = [&]() {return ++value; };
// 캡펴한 값을 const 타입으로 캐스팅해 캡쳐
auto incrementSafe = [&immutableValue = as_const(value)]()
{return immutableValue + 1; };
사실 코드 길이를 늘려나가는 것보다, 일반적으로는 값에 의한 캡쳐를 사용하는 것이 좋다! 이는 포인터 타입의 경우에도 마찬가지이다.
마지막으로, 복소수를 string 으로 출력해 주는 람다를 보면서 이 글을 마무리하자.
class Imaginary
{
private:
int real;
int imaginary;
public:
Imaginary() : real(0), imaginary(0) {}
Imaginary(int _real) : real(_real), imaginary(0) {}
Imaginary(int _real, int _imaginary) : real(_real), imaginary(_imaginary) {}
public:
/** 복소수를 출력해 주는 람다함수 */
function<string()> toStringLambda = [this]()
{
return to_string(real) + " + " + to_string(imaginary) + "i";
};
};
참고로, this 로 모든 것을 캡쳐하는 것은 별로 좋은 방식이 아니다. 대신, static 함수를 만들고, 이를 변수로 할당해 사용하는 방법도 알아두자 😁
class Imaginary
{
/* ... */
static function<string(const Imaginary&)> toStringLambdaStatic;
};
/** 정적 함수로 만들고, 변수로 할당하여 사용 */
function<string(const Imaginary&)> Imaginary::toStringLambdaStatic =
[](const Imaginary& Input)
{
return to_string(Input.real) + " + " + to_string(Input.imaginary) + "i";
};
int main()
{
Imaginary im(1, 2);
auto toStringLambdaLocal = Imaginary::toStringLambdaStatic;
cout << toStringLambdaLocal(im) << endl;
}
'R & D > Software Engineering' 카테고리의 다른 글
[C++ 함수형 프로그래밍] 함수로 설계하기 (feat. 틱택토와 STL) (0) | 2023.09.28 |
---|---|
[C++ 함수형 프로그래밍] 파셜 애플리케이션과 커링 (0) | 2023.09.28 |
[C++ 함수형 프로그래밍] 함수형 합성 (C++ 에서 함수 합성하기) (0) | 2023.09.28 |
UML (클래스 다이어그램, 시퀀스 다이어그램) 간단 정리 (0) | 2022.10.14 |
[책 리뷰] 객체 지향의 사실과 오해 리뷰 (0) | 2022.08.29 |