KoreanFoodie's Study

[C++ 함수형 프로그래밍] 함수로 설계하기 (feat. 틱택토와 STL) 본문

R & D/Software Engineering

[C++ 함수형 프로그래밍] 함수로 설계하기 (feat. 틱택토와 STL)

GoldGiver 2023. 9. 28. 13:19

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

핵심 :

1. 모든 프로그램은 순수 함수의 집합으로 표현될 수 있으며, 입력과 출력이 상호 작용하는 함수 집합으로 정의할 수 있다.

2. 입력과 출력 중심으로 사고하며, 중간 단계의 변환 과정을 차례대로 구현해 보자. 

함수형으로 사고하기

모든 프로그램은 순수 함수의 집합으로 표현될 수 있으며, 입력과 출력이 상호 작용하는 함수 집합으로 정의할 수 있다.

함수를 설계할 때, 다음 스텝을 밟으면 함수를 간단하게 설계할 수 있다.

  1. 입력 데이터로부터 출발한다.
  2. 출력 데이터를 정의한다.
  3. 일련의 입력 데이터를 출력 데이터로 변환하는 각 단계인 변형 과정(순수 함수)를 정의한다.

사실 위 내용은 말로만 들어서는 감이 잘 안 올 수 있다. 그래서 예제를 곁들이면 참~ 좋은데... 책에서는 틱택토 게임을 만들면서 이 내용을 다뤘다.

하지만... 포스팅에서 모든 내용을 따라가며 설명하는 것은 너무 피곤한 일이다. 😖 그래서 개념적으로만 생각해 보자.

틱택토 게임에서는 한 줄이 O 나 X 가 되면 승자가 결정된다.

틱택토 게임에서 승자를 가려야 하는데, 그럼 일단 각 라인(행, 열, 대각선)이 한 문자로 가득차 있는지 체크해야 한다.

필요한 함수의 흐름을 생각하면 다음과 같다 :

1. filledWithX, filledWithO 가 필요하네?
->그러려면 한 줄을 체크하는 로직이 필요하다.

2. any_of 를 활용해서 전체 컬렉션을 순회해야겠다!
->any_of 를 순회할 때 각 요소에 대해 체크할 predicate 가 필요하네?

3. 캐릭터가 'O' 인지 'X' 인지 체크해야 하네?
-> equalChar(char val) 함수가 필요하네?

이런 식으로 꼬리에 꼬리를 물고 구현하면 될 것이다.

실제로 위와 같은 사고방식은 TDD(Test Driven Development; 테스트 주도 개발) 개발에도 잘 맞아 떨어진다. 빠르게 실패하고, 빠르게 고치자 😉

 

또한 위와 같은 식으로 여러 함수가 만들어 졌을 때, 각 쓰임새나 인자 타입별로 순수 함수들을 묶을 수 있을 것이다.

OOP 의 핵심이 객체간의 상호작용이라는 점을 기억해 보면, 성질이 비슷한 순수함수들을 하나의 클래스에 묶어 응집도를 올림으로써 OOP 방식의 철학을 지킬 수 있게 된다. 😝 OOP 와 함수형 패러다임은 서로 대립하는 관계가 아님을 기억하자!

 

마지막으로, 이번 글에서는 각 컬렉션을 다루는 데 유용한 여러 예제 코드를 소개하고 기록을 마치고자 한다 😅

// 전체 컬렉션 순회하며 fn 적용
auto AllOfCollection = [](const auto& collection, auto fn)
{
	return all_of(collection.begin(), collection.end(), fn);
};


// transform 이용하여 변환 컬렉션 만들기 
// 출력 타입이 입력 타입과 일치하지 않아도 된다는 것이 놀랍다!
template<typename DestType>
auto TransformAll = [](const auto& source, auto fn)
{
	DestType result;
	transform(source.begin(), source.end(), back_inserter(result), fn);
	return result;
};


// 범위 구하기 (Range 개념은 C++ 20 에서 표준으로 채택되었다)
// iota 는 시작 인자부터 1씩 더해가며 컬렉션을 채워주는 함수이다
auto ToRange = [](const auto& collection)
{
	vector<int> result(collection.size());
	iota(begin(result), end(result), 0);
	return result;
};


// 전체 컬렉션 중 조건을 만족하는 녀석이 있는지 체크
auto AnyOfCollection = [](const auto& collection, auto fn)
{
	return any_of(collection.begin(), collection.end(), fn); 
};


// 컬렉션이 문자열일 때, 문자열을 합치는 것 따위에 이용될 수 있다.
// string<vector> i{"1", "2", "3"} -> "123"
auto AccumulateAll = [](const auto& collection, auto fn)
{
	return accumulate(collection.begin(), collection.end(), typename decltype(collection)::value_type(), fn);
};


// 전체 컬렉션 중 조건에 맞는 녀석을 반환
// 맞는 것을 찾지 못했을 경우, optional 을 이용해 에러를 체크한다.
auto FindInCollection = [](const auto& collection, auto fn)
{
	auto result = find_if(collection.begin(), collection.end(), fn);
	return (result == collection.end()) ? nullopt : optional(*result);
};


// 컬렉션 중 조건을 만족하는 것이 없는지 체크
auto NoneOf = [](const auto& collection, auto fn)
{
	return none_of(collection.begin(), collection.end(), fn);
};

 

Comments