KoreanFoodie's Study

[C++ 함수형 프로그래밍] 함수형 연산자를 활용한 중복 제거 본문

R & D/Software Engineering

[C++ 함수형 프로그래밍] 함수형 연산자를 활용한 중복 제거

GoldGiver 2023. 9. 28. 17:21

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

핵심 :

1. 함수형 연산자를 활용해 중복을 제거하고 DRY 원칙을 지킬 수 있다.

2. 파라미터 유사성이나 함수 중복 사용의 유사성은 파셜 애플리케이션 특수화나 함수형 합성으로 해결할 수 있다.

3. 고차원 함수를 통해 구조적 유사성과 숨은 루프를 제거할 수 있지만, 과도한 추상화는 다른 프로그래머들에게 진입장벽을 높일 수 있음에 유의하자.

함수형 연산자를 활용한 중복 제거

함수형 연산자를 적극 활용함으로써, 중복을 제거하고 DRY 원칙을 지킬 수 있다.

DRY 원칙은 Don't Repeat Yourself 의 약자로, 코드가 지식을 보관하는 방식이라는 이해에 바탕을 둔다. 이상적으로는 시스템에 존재하는 지식은 중복되면 안되며, 우리가 찾는 뭔가는 반드시 한 장소에 존재할 것이다.

이와는 대척점에 있는 원칙이 바로 WET 이다. WET 은 여러 별명을 가지고 있는데, 대표적으로 아래와 같은 것들이 있다 🤣

  • Write Everyting Twice
  • We Enjoy Typing
  • Waste Everyone's Time

 

이와 반대로, 간단한 설계는 다음 내용을 따른다.

  • 테스트를 통과한다.
  • 의도를 드러낸다.
  • 중복을 제거한다.
  • 구성 요소의 수가 더 적다.

만약 개발 중 유사성을 발견했을 경우, 유사성을 제거할지 말지를 결정하고 정리하자.

 

아래는 유사성을 정리하는 대표적인 케이스들이다.

파셜 애플리케이션을 활용해 파라미터의 유사성 정리하기

// 파라미터의 유사성을 발견
auto lineFilledWith = [](const auto & line, const auto tokenToCheck)
{
	return all_of_collection(line, [&tokenToCheck](const auto token)
		{
			return token == tokenToCheck;
		};
	);
};

// 파셜 애플리케이션으로 특수화
auto lineFilledWithX = bind(lineFilledWith, _1, 'X');
auto lineFilledWithO = bind(lineFilledWith, _1, 'O');

 

다른 함수의 출력으로 함수를 호출하는 유사성을 함수형 합성으로 대체하기

다음과 같은 흐름이 있다고 보자.

void processA()
{
	a = f1(...);
	b = f2(a, ...);
	c = f3(b, ...);
}

void processB()
{
	a = f1Prime(...);
	b = f2(a, ...);
	c = f3(b, ...);
}

함수형 합성을 이용하면, 이를 다음과 같이 분리해낼 수 있다 :

// 각 프로세스의 흐름은 다음과 같다 :
processA = f3(f2(f1...));
processB = f3(f2(f1Prime...));

// 따라서 아래처럼 유사성을 고를 수 있다.
C = f3(f2(...));
processA = C(f1(...));
processB = C(f1Prime(...));

 

고차원 함수를 활용한 구조적 유사성 제거하기

이 방법은 기존 코드의 유사성을 추출해 파셜 애플리케이션을 만들고, 고차원 함수를 생성하는 추상화 단계를 거친다. 이는 람다와 파셜 애플리케이션의 연결고리를 이해하면 쉽다. 예를 들어 다음 코드가 있다고 해 보자.

// 틱택토 게임에서 승자를 판별하는 람다
auto tokenWins = [](const auto& board, const auto& token)
{
	return any_of_collection(allLinesColumnsAndDiagnals(board),
		[token](auto line) 
		{
			return lineFilledWith(line, token);
		});
};

가장 안쪽의 람다를 보면, 사실 이것을 파셜 애플리케이션으로 치환할 수 있음을 눈치챌 수 있다.

auto tokenWins = [](const auto& board, const auto& token)
{
	return any_of_collection(allLinesColumnsAndDiagnals(board),
		bind(lineFilledWith, _1, token));
};

이제 우리는 tokenWins 의 기능을 하는 함수를 생성할 수 있게 되었다!

// i -> board, j -> token
template <typename F, typename G, typename H>
auto foo(F f, G g, H h)
{
	return [=](auto i, auto j)
	{
		return f(g(i), bind(h, _1, j));
	};
}

auto tokenWins = foo(any_of_collection, allLinesColumnsAndDiagnals, lineFilledWith);
auto result = tokenWins(board, 'X');

읽기 어려우니 나중에는 이름을 더 명확하게 바꾸겠지만, 이 과정에서 상대방이 코드를 이해하기 어렵게 만들게 된다. 따라서 추상화를 할 때는 사이드 이펙트를 고려해야 한다.

 

고차원 함수를 활용해 숨은 루프 제거하기

명령형 언어로 개발을 하다 보면, 다음과 같은 문장을 거의 매일 같이 보고 쓰게 된다.

if(condition) return value;

우리는 이것이 중복이 아니라고 생각하지만, 루프 내에서 이것은 사실 숨은 루프이다.

다시... 틱택토 문제로 되돌아와 승자를 찾는 코드를 생각해 보자. 일반적으로는, if 문을 반복해 가며 X 부터 승자를 찾았을 것이다.

// 다음과 같은 승리 관련 조건을 만든다
auto xWins = bind(tokenWins, _1, 'X');
auto oWins = bind(tokenWins, _1, 'O');
auto gameNotOverYet = [](auto board) {return board.notFilledYet(); };
auto True = []() {return true; };

// 보드와 승리 관련 조건을 묶는다 :
auto xWinsOnBoard = bind(xWins, board);
auto oWinsOnBoard = bind(oWins, board);
auto gameNotOverYetOnBoard = bind(gameNotOverYet, board);


// 전통적인 if 문 활용 방식이다
if (xWinsOnBoard())
	return XWins;
if (oWinsOnBoard())
	return OWins;
if (gameNotOverYetOnBoard())
	return GameNotOverYet;

return Draw;

마지막 if 문 나열 부분을 find_if 를 이용해 규칙 리스트를 순회하도록 만들면, 다음과 같이 표현할 수 있다.

vector<pair<function<bool()>, Result>> rules =
{
	{xWins, XWins},
	{oWins, OWins},
	{gameNotOverYet, GameNotOverYet},
	{True, Draw}
};
auto theRule = find_if(rules.begin(), rules.end(), [](auto pair)
	{
		return pair.first();
	});
// theRule 은 항상 값을 가지며, {True, Draw} 를 기본 값으로 가진다.
return theRule->second;

위 코드를 사용함으로써, 새로운 조건과 결과 쌍을 쉽게 추가할 수 있게 되었다! 규칙 리스트의 마지막이 {True, Draw} 이므로, 이전에 성립하는 규칙이 없을 경우 무승부가 도출될 것이다.

마지막으로, theRule 에서 결과를 추출하는 부분을 다음과 같이 추상화할 수 있다! 😄

auto condition = [](auto rule)
{
	rule.first;
};

auto result = [](auto rule)
{
	rule.second;
};

auto findTheRule = [](const auto& rules)
{
	return *find_if(rules.begin(), rules.end(), [](const auto& rule)
		{
			condition(rule);
		});
};

auto findTheRuleThatFirstApplies = [](const auto& rules)
{
	return result(findTheRule(rules));
};

/***/

// 룰을 찾는 부분이 아래 코드 한줄로 바뀜
return findTheRuleThatFirstApplies(rules);
Comments