KoreanFoodie's Study
[C++ 함수형 프로그래밍] 함수형 연산자를 활용한 중복 제거 본문
함수형 프로그래밍 패러다임에 대해 알아보며 이를 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);
'R & D > Software Engineering' 카테고리의 다른 글
[C++ 함수형 프로그래밍] 특성 기반 테스트 , 모나드, 이벤트 소싱 (0) | 2023.10.04 |
---|---|
[C++ 함수형 프로그래밍] 퍼포먼스 최적화(메모이제이션과 꼬리 재귀, 병렬 연산) (0) | 2023.09.28 |
[C++ 함수형 프로그래밍] 함수로 설계하기 (feat. 틱택토와 STL) (0) | 2023.09.28 |
[C++ 함수형 프로그래밍] 파셜 애플리케이션과 커링 (0) | 2023.09.28 |
[C++ 함수형 프로그래밍] 함수형 합성 (C++ 에서 함수 합성하기) (0) | 2023.09.28 |