더 좋은 코드를 위한 함수형 프로그래밍
모던 C++를 중심으로
Blizzard Entertainment
전이삭
오늘 살펴볼 개념들
• 순수함수와 불변성
• 고차함수와 함수의 합성
• 타입 시스템과 대수적 자료형
• 타입을 매개로 한 함수의 합성 *
순수함수 Pure functions
순수함수 - Pure functions
• 함수의 결과값이 오직 입력 인자 값들에 의해서만 결정
• 특정 입력 값에 대해서 결과값이 결정적으로 대응되는 함수
• 부작용(Side effects)이 없는 함수
int abs(int n);
int atoi(const char *str);
int getDefaultPort(bool https) {
return https ? 443 : 80;
}
int rand();
time_t time (time_t* timer);
bool g_https = false;
int getDefaultPort() {
return g_https ? 443 : 80;
}
부작용 - Side effects
• 상태에 영향을 받거나 변화를 주는 모든 동작
• 화면출력
• 소켓통신
• 디스크 I/O
• 예외 catch
• 현재 시각 읽기
• 난수 발생
참조 투명성 - Referential transparency
• 어떤 표현식을 그 표현식의 결과값으로 교체해도 전체
프로그램의 실행결과에 영향을 주지 않는 성질
• 컴파일러에게 다양한 최적화 기회를 제공
기억 – Memoization
- 입력값 / 함수명을 알면 결과값은 항상 같음
지연연산 - Lazy evaluation
- 실제로 결과값이 필요할 때만 함수를 평가
• 코드에서 가능한 순수함수의 비율이 많아지도록 하자
- 글로벌 변수, 멤버 변수 의존도를 줄이자
- 상태를 없애거나 줄이고 부작용을 격리시키자
• 함수는 인자를 받아서 값을 리턴하는 형태로
- 레퍼런스 인자 보다는 값을 리턴
- 여러 개의 값을 리턴할 때는 tuple을 사용
FP 실천사항 #1 - 순수함수
불변성 Immutability
Java String is immutable
String str = "Welcome to NDC2016!!!";
String newString =
str.replace(“2016”, “2017”);
System.out.println(newString);
String str = "Welcome to NDC2016!!!";
str.replace(“2016”, “2017”);
System.out.println(str);
Welcome to NDC2016!!! Welcome to NDC2017!!!
불변객체의 특징
• 내용의 변경은 새로운 객체를 생성할 때만 가능
• 생성, 테스트, 사용법이 단순하고 쉬움
• 진정한 의미의 불변객체는 그 자체로 Thread-safe
• 쉬운 캐쉬 – 이름이 같으면 내용도 같다
• Temporal coupling 줄임
• Identity mutability problem 없음
Temporal coupling
HttpRequest request;
request.SetUrl("https://hostname/api");
request.SetSSL(true);
request.SetCACert("ca-bundle.crt");
request.Post();
HttpRequest request;
request.SetUrl("https://hostname/api");
request.SetSSL(true);
request.Post(); // Error!
class HttpRequest {
public:
HttpRequest(const std::string& url, bool ssl,
const std::string& cacert);
// no setters
...
};
HttpRequest request("https://hostname/api", true,
"ca-bundle.crt");
request.Post();
Identity mutability problem
class Monster {
public:
…
int64_t id() const { return id_; }
std::string name() const { return name_; }
void setID(int64_t id) { id_ = id; }
void setName(const std::string& name) {
name_ = name;
}
…
};
using MonsterPtr = std::shared_ptr<Monster>;
std::map<int64_t, MonsterPtr> monsters;
MonsterPtr p1 =
std::make_shared<Monster>(1234, “Murloc");
monsters.insert(std::make_pair(p1.id(), p1));
p1->setID(2234); // ???
Memory와 Disk 가격
CPU 클럭 속도의 한계
• 사용하는 객체를 불변으로 일단 바꾸어 놓고 코드를
고쳐보자
- Setter들 대신에 생성자에서 인자로 초기화
- 객체를 변경하지 않는 멤버함수들은 const로
• 다양한 분산 빅데이터 알고리즘들을 조사해보자
- Immutability changes everything -
http://queue.acm.org/detail.cfm?id=2884038
• 영속형 자료 구조에 대해서 알아보자
FP 실천사항 #2 - 불변객체
고차함수와 함수의 합성
고차함수
• 함수의 인자로 한 개 이상의 함수를 넘겨 받거나,
함수의 결과값으로 다른 함수를 돌려주는 함수
• 고차함수의 보물창고 - #include <algorithm>
FP의 기본 초식 3총사
• std::transform
• std::remove_if / std::copy_if
• std::accumulate
std::accumulate
using Strings = std::vector<std::string>;
int64_t hash(const std::string& str, int64_t seed);
int64_t calculate_hash_by_stl(const Strings& strs) {
return std::accumulate(std::begin(strs),
std::end(strs), 12345ll,
[](int64_t seed, const std::string& s)
{ return hash(s, seed); });
}
추천 동영상 – CppCon 2016 "std::accumulate: Exploring an Algorithmic
Empire“ - https://youtu.be/B6twozNPUoA
Unix 명령어들의 합성 - PIPE
history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head
47 ls
26 cd
19 sudo
17 clang++
10 clang
7 history
7 ./a.out
5 ./compose
4 gedit
2 top
예제 – 몬스터에게 범위공격
• 특정 거리 r 이상 R 이하에
있는 대상을 공격
• 방어막이 있으면 방어막의
세기만큼 피해 감소
Dev-art by TH. Ahn
Monster class
class Monster {
public:
Monster();
void setId(int id);
void setHP(int hp);
void setArmor(int armor);
void setLocation(const Point& pt);
int getHP() const;
int getId() const;
bool getArmor() const;
Point getLocation() const;
void gotDamage(int damage);
private:
int id_;
int hp_;
int armor_;
Point pt_;
};
using MonsterPtr = std::shared_ptr<Monster>;
using Monsters = std::vector<MonsterPtr>;
void Monster::gotDamage(int damage) {
hp_ = hp_ - std::min(hp_, damage - armor_);
}
int calc_distance(const Point& pt1, const
Point& pt2);
Monsters doSplashDamage(const Point& my_location, const Monsters& monsters,
int damage, int range_from, int range_to) {
for (auto i = monsters.begin(); i != monsters.end(); ++i) {
auto pt = *i;
Monster& target = *pt;
int distance = calc_distance(my_location, target.getLocation());
if (distance >= range_from && distance <= range_to) {
target.gotDamage(damage);
}
}
Monsters alive_monsters;
for (auto i = monsters.begin(); i != monsters.end(); ++i) {
auto pt = *i;
Monster& target = *pt;
if (target.getHP() > 0) {
alive_monsters.push_back(pt);
}
}
return alive_monsters;
}
전형적인 절차형 코드
우리가 하고 싶은 것들
• 객체를 불변데이터로 다루고 싶다
• 가능한 순수함수를 쓰고 싶다
• 알고리즘 구현 부분과 사용 부분을 코드상에서 분리하고
싶다
• 임시변수들을 없애고 싶다
• 함수들을 쉽게 합성하고 싶다
불변 Monster class와 피해계산 순수함수
class Monster {
public:
Monster(int id, int hp,
int armor, const Point& pt);
int getHP() const;
int getId() const;
int getArmor() const;
Point getLocation() const;
static int calc_damaged_hp(int hp,
int damage, int armor);
private:
int id_;
int hp_;
int armor_;
Point pt_;
};
int Monster::calc_damaged_hp(int hp,
int damage, int armor) {
return hp - std::min(hp, damage - armor);
}
주요 문장들을 함수로 쪼개기
bool is_in_range(const Point& my_location, int range_from, int range_to, const MonsterPtr& pm);
MonsterPtr damaged_monster(int damage, const MonsterPtr& pm);
bool is_alive(const MonsterPtr& pm);
Partial application
bool is_in_range_(const Point& my_location, int range_from, int range_to, const MonsterPtr& pm);
bool partially_applied_is_in_range_(ANY_FIXED_LOC, ANY_FIXED_r, ANY_FIXED_R, const MonsterPtr& pm);
bool still_a_function_with_only_one_parameter(const MonsterPtr& pm);
Lambda로 Partial application 1/2
auto is_in_range_(const Point& my_location, int range_from, int range_to) {
return [my_location, range_from, range_to](const MonsterPtr& pm) {
int distance = calc_distance(my_location, pm->getLocation());
return distance >= range_from && distance <= range_to;
};
}
auto damaged_monster_(int damage) {
return [damage](const MonsterPtr& pm) {
return std::make_shared<Monster>(pm->getId(),
Monster::calc_damaged_hp(pm->getHP(), damage, pm->getArmor()),
pm->getArmor(), pm->getLocation());
};
}
Lambda로 Partial application 2/2
auto do_damage_(const Point& my_location, int damage,int range_from, int range_to) {
return [=](const MonsterPtr& pm) {
return is_in_range_(my_location, range_from, range_to)(pm) ? damaged_monster_(damage)(pm) : pm;
};
}
bool is_alive(const MonsterPtr& pm) {
return pm->getHP() > 0;
}
STL algorithm 이용하기
void doSplashDamage_stl(const Point& my_location, Monsters& monsters,
int damage, int range_from, int range_to) {
std::transform(monsters.begin(), monsters.end(), monsters.begin(),
do_damage_(my_location, damage, range_from, range_to));
auto is_dead = [](const auto& m) { return !is_alive(m); };
monsters.erase(std::remove_if(monsters.begin(), monsters.end(), is_dead), monsters.end());
}
“누가 네 줄로 짤 코드를 열줄로 만들어 놨어요?”
Ranges TS
Most effective STL using ranges
std::vector<int> numbers = { 1, 2, 3, 4, 5, 6, 7 };
std::vector<int> output;
auto is_even = [](int i) { return i % 2 == 0; };
auto mul_2 = [](int i) { return i * 2; };
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(output), is_even);
std::transform(output.begin(), output.end(), output.begin(), mul_2);
int sum = std::accumulate(output.begin(), output.end(), 0);
auto rng = numbers
| ranges::view::filter(is_even)
| ranges::view::transform(mul_2);
int sum = ranges::accumulate(rng, 0);
// numbers is still {1, 2, 3, 4, 5, 6, 7};
ranges::view로 algorithm을 합성하기
auto doSplashDamage_one_liner(const Point& my_location, const Monsters& monsters,
int damage, int range_from, int range_to) {
return monsters
| ranges::view::transform(do_damage_(my_location, damage, range_from, range_to))
| ranges::view::filter(is_alive);
}
“누가 한 줄로 짤 코드를 열줄로 만들어 놨어요?”
합성으로 구현한 코드의 손쉬운 변경
auto doSplashDamage_one_liner(const Point& my_location, const Monsters& monsters,
int damage, int range_from, int range_to) {
return monsters
| ranges::view::transform(do_damage_(my_location, damage, range_from, range_to))
| ranges::view::transform(apply_healing_(hp, range_from, range_to))
| ranges::view::filter(is_alive);
}
“기존의 코드는 건드리지 조차 않아도 되니 안심!”
ranges의 지연연산
auto doSplashDamage_top_3(const Point& my_location, const Monsters& monsters,
int damage, int range_from, int range_to) {
return monsters
| ranges::view::transform(do_damage_(my_location, damage, range_from, range_to))
| ranges::view::filter(is_alive)
| ranges::view::take(3);
}
쓰지 않을 객체는 만들지도 말고, 필요 없는 계산은 하지도
말자!
std::ranges가 되고 싶은 Ranges TS
• STL을 더욱 효과적으로 활용
• 불변 컨테이너
• 합성 / 지연연산 / Monad *
• https://github.com/ericniebler/range-v3
• https://github.com/Microsoft/Range-V3-VS2015
• 고차함수를 사용해서 코드를 간결하게 만들자
- 간결한 코드 -> 가독성 -> 적은 버그
• 알고리즘의 구현 부분과 사용 부분을 분리하자
- 로직 구현에 실제로 관련 없는 임시변수와 상태들을
없애자
• 함수를 합성 가능하게 만들어서 재활용성을 높이자
- 검증된 재활용이 쉬운 Building block들을 합성
• 지연연산을 활용해서 최적화를 시도하자
- 무한리스트에 대해서 알아보자
FP 실천사항 #3 – 고차함수와 합성
타입시스템의 활용
타입의 활용
• 메모리상에 저장된 데이터가 쓰일 방법을 정의
• 가능한 빠른 단계에서 오류 발견
• 코드의 의미 함축성 증가
• 코드 작성중의 힌트
• 로직 설계의 도구
• 지원하지 않는 동작은 표현 자체가 불가능 하도록
• 합성 동작의 연결을 위한 접착제
std::optional in C++17
void optional_demo() {
std::optional<std::string> name;
assert(!name);
if (name) std::cout << *name << std::endl;
std::optional<std::string> a_name("Anonymous");
assert(a_name);
assert(a_name.value() == "Anonymous");
assert(*a_name == "Anonymous");
if (a_name) std::cout << *a_name << std::endl;
}
• null – “The worst mistake of computer science”
• Type safe / no dynamic allocation
std::variant in C++17
std::variant<std::string, bool, int> v = std::string("Hello!");
assert(std::get<std::string>(v) == "Hello!");
std::variant<std::string, bool, int> b = true;
assert(std::get<bool>(b) == true);
try {
std::cout << std::boolalpha << std::get<bool>(v) << std::endl;
} catch (std::bad_variant_access&) {
std::cout << "exception..." << std::endl;
}
• Discriminated union
• Never-empty guarantee
• Type safe / no dynamic allocation
Quiz #1
struct Human {
unsigned char age_;
bool alive_;
};
다음 타입은 몇 개의 서로 다른 값(상태)를 나타낼 수
있을까요?
256 * 2 = 512
using A_Tuple_Type = std::tuple<unsigned char, bool>;
Quiz #2
enum Gender : int {
MALE,
FEMALE,
UNKNOWN
};
struct Human {
Gender gender_;
unsigned char age_;
bool alive_;
};
Human은 몇 개의 서로 다른 값(상태)를 나타낼 수
있을까요?
3 * 256 * 2 = 1536
Quiz #3
using Another_Tuple_Type = std::tuple<Gender, unsigned char, bool>;
다음 타입은 몇 개의 서로 다른 값(상태)를 나타낼 수
있을까요?
3 * 256 * 2 = 1536
동등한 타입
struct Human {
Gender gender_;
unsigned char age_;
bool alive_;
};
std::tuple<Gender, unsigned char, bool>;
Quiz #4
std::optional<Human>;
다음 타입은 몇 개의 서로 다른 값(상태)를 나타낼 수
있을까요?
1536 + 1 = 1537
Quiz #5
struct Alien { bool alive_; };
std::variant<Human, Alien>;
다음 타입은 몇 개의 서로 다른 값(상태)를 나타낼 수
있을까요?
1536 + 2 = 1538
대수적 자료형 - Algebraic data types (ADTs) in
C++
• 타입도 합성
• Product types (*) – std::pair, std::tuple, struct/class
• Sum types (+) – std::optional, std::variant
ADTs로 표현 한다면? – 타입의 합성
class HttpRequest {
//...
private:
Url url_;
SecureUrl url_ssl_;
bool ssl_;
std::string id_;
std::string password_;
bool need_auth_;
};
class HttpRequest {
//...
private:
std::variant<Url, SecureUrl> url_;
using Credential =
std::tuple<std::string, std::string>;
std::optional<Credential> auth_;
};
• 유효한 상태의 개수보다 데이터 타입의 Cardinality가
크다면?
무효한 상태를 가질 가능성 -> 버그!
• 타입을 최대한 사용해서 안전한 코드를 만들자
- 불가능한 동작은 표현 자체가 불가능하도록
• 타입이 표현가능한 상태의 개수(Cardinality)를 계산해보자
- Product 타입, Sum 타입
• 유효하지 않은 상태를 가질 수 없는 타입을 정의하자
FP 실천사항 #4 – 타입 시스템
모나드 겉핥기
Download a file and decompress
it
std::tuple<int, std::string> download(const std::string& url);
bool verify(const std::string& body);
bool decompress(const std::string& body, std::string& uncompressed);
std::string imperative(const std::string& url) {
auto r = download(url);
int status_code = std::get<int>(r);
const std::string& body = std::get<std::string>(r);
if (status_code == 200) {
bool verified = verify(body);
if (verified) {
std::string decompressed;
bool success = decompress(body, decompressed);
if (success) {
return decompressed;
}
}
}
return std::string();
}
std::optional 을 사용하면
std::optional<std::string> download_fn(const std::string& url) {
auto r = download(url);
int status_code = std::get<int>(r);
const std::string& body = std::get<std::string>(r);
return status_code == 200 ? std::make_optional(body) : std::nullopt;
}
std::optional<std::string> verify_fn(const std::string& body) {
bool verified = verify(body);
return verified ? std::make_optional(body) : std::nullopt;
}
std::optional<std::string> decompress_fn(const std::string& body) {
std::string decompressed;
bool success = decompress(body, decompressed);
return success ? std::make_optional(decompressed) : std::nullopt;
}
조금 단순화된 if else
문들
std::string do_download(const std::string& url) {
auto body = download_fn(url);
if (body) {
auto verified = verify_fn(*body);
if (verified) {
auto decompressed = decompress_fn(*verified);
if (decompressed)
return *decompressed;
}
}
return std::string();
}
template <typename A>
std::optional<A> unit_(A a) {
return std::make_optional(a);
}
template <typename F>
std::optional<std::string> bind_(const std::optional<std::string>& v, F func) {
return v ? func(*v) : std::nullopt;
}
Define two functions
• 이 두개의 함수를 이용해서 표준적인 방법으로 입출력
타입이 호환되는 함수들을 합성하자!
Using Maybe Monad
void monad_demo() {
auto download_ = [](const std::string& url) {
auto r = download(url);
int status_code = std::get<int>(r);
const std::string& body = std::get<std::string>(r);
return status_code == 200 ? unit_(body) : std::nullopt;
};
auto verify_ = [](const std::string& body) {
bool verified = verify(body);
return verified ? unit_(body) : std::nullopt;
};
auto decompress_ = [](const std::string& body) {
std::string decompressed;
bool success = decompress(body, decompressed);
return success ? unit_(decompressed) : std::nullopt;
};
auto m = [=](auto v) {
return bind_(bind_(bind_(unit_(v), download_), verify_), decompress_);
};
auto result = m(std::string("http://ndc.nexon.com/files/testdata.txt"));
if (result) std::cout << *result << std::endl;
}
auto m = [=](auto v) {
return bind_(bind_(bind_(unit_(v), download_), verify_), decompress_);
};
마지막으로 연산자를 오버로딩 하면
• ranges::view 들을 합성하던 것과 같은 방법!
auto m = [=](auto v) {
return v | download_ | verify_ | decompress_;
};
Monad
A monad is created by defining a type constructor M and two operations, bind and return
(where return is often also called unit):
• The unary return operation takes a value from a plain type (a) and puts it into a container
using the constructor, creating a monadic value (with type M a).
• The binary bind operation ">>=" takes as its arguments a monadic value with type M a and
a function (a → M b) that can transform the value.
• The bind operator unwraps the plain value with type a embedded in its input monadic
value with type M a, and feeds it to the function.
• The function then creates a new monadic value, with type M b, that can be fed to the next
bind operators composed in the pipeline.
https://en.wikipedia.org/wiki/Monad_(functional_programming)
Monad in C++
• Maybe monad using std::optional
• Ranges TS - std::ranges in C++20
• Composable std::future in C++20
요점정리
• 상태가 필수적인 경우가 아니라면 가능한 순수함수를
만들자
• 불변데이터와 관련 알고리즘들을 이해하고 활용하자
• 고차함수를 사용해서 코드를 간결하게 하자
• 알고리즘의 구현과 사용 부분을 코드상에서 분리하자
• 타입을 적극 활용해서 안전한 코드를 만들자
• 합성을 사용해서 코드의 변경을 쉽게 하고 재활용성을
높이자
• 함수형 프로그래밍 언어를 한가지 공부해보자 - Haskell
강추!
감사합니다.

Nexon Developers Conference 2017 Functional Programming for better code - Modern C++

  • 1.
    더 좋은 코드를위한 함수형 프로그래밍 모던 C++를 중심으로 Blizzard Entertainment 전이삭
  • 2.
    오늘 살펴볼 개념들 •순수함수와 불변성 • 고차함수와 함수의 합성 • 타입 시스템과 대수적 자료형 • 타입을 매개로 한 함수의 합성 *
  • 3.
  • 4.
    순수함수 - Purefunctions • 함수의 결과값이 오직 입력 인자 값들에 의해서만 결정 • 특정 입력 값에 대해서 결과값이 결정적으로 대응되는 함수 • 부작용(Side effects)이 없는 함수 int abs(int n); int atoi(const char *str); int getDefaultPort(bool https) { return https ? 443 : 80; } int rand(); time_t time (time_t* timer); bool g_https = false; int getDefaultPort() { return g_https ? 443 : 80; }
  • 5.
    부작용 - Sideeffects • 상태에 영향을 받거나 변화를 주는 모든 동작 • 화면출력 • 소켓통신 • 디스크 I/O • 예외 catch • 현재 시각 읽기 • 난수 발생
  • 6.
    참조 투명성 -Referential transparency • 어떤 표현식을 그 표현식의 결과값으로 교체해도 전체 프로그램의 실행결과에 영향을 주지 않는 성질 • 컴파일러에게 다양한 최적화 기회를 제공 기억 – Memoization - 입력값 / 함수명을 알면 결과값은 항상 같음 지연연산 - Lazy evaluation - 실제로 결과값이 필요할 때만 함수를 평가
  • 7.
    • 코드에서 가능한순수함수의 비율이 많아지도록 하자 - 글로벌 변수, 멤버 변수 의존도를 줄이자 - 상태를 없애거나 줄이고 부작용을 격리시키자 • 함수는 인자를 받아서 값을 리턴하는 형태로 - 레퍼런스 인자 보다는 값을 리턴 - 여러 개의 값을 리턴할 때는 tuple을 사용 FP 실천사항 #1 - 순수함수
  • 8.
  • 9.
    Java String isimmutable String str = "Welcome to NDC2016!!!"; String newString = str.replace(“2016”, “2017”); System.out.println(newString); String str = "Welcome to NDC2016!!!"; str.replace(“2016”, “2017”); System.out.println(str); Welcome to NDC2016!!! Welcome to NDC2017!!!
  • 10.
    불변객체의 특징 • 내용의변경은 새로운 객체를 생성할 때만 가능 • 생성, 테스트, 사용법이 단순하고 쉬움 • 진정한 의미의 불변객체는 그 자체로 Thread-safe • 쉬운 캐쉬 – 이름이 같으면 내용도 같다 • Temporal coupling 줄임 • Identity mutability problem 없음
  • 11.
    Temporal coupling HttpRequest request; request.SetUrl("https://hostname/api"); request.SetSSL(true); request.SetCACert("ca-bundle.crt"); request.Post(); HttpRequestrequest; request.SetUrl("https://hostname/api"); request.SetSSL(true); request.Post(); // Error! class HttpRequest { public: HttpRequest(const std::string& url, bool ssl, const std::string& cacert); // no setters ... }; HttpRequest request("https://hostname/api", true, "ca-bundle.crt"); request.Post();
  • 12.
    Identity mutability problem classMonster { public: … int64_t id() const { return id_; } std::string name() const { return name_; } void setID(int64_t id) { id_ = id; } void setName(const std::string& name) { name_ = name; } … }; using MonsterPtr = std::shared_ptr<Monster>; std::map<int64_t, MonsterPtr> monsters; MonsterPtr p1 = std::make_shared<Monster>(1234, “Murloc"); monsters.insert(std::make_pair(p1.id(), p1)); p1->setID(2234); // ???
  • 13.
  • 14.
  • 15.
    • 사용하는 객체를불변으로 일단 바꾸어 놓고 코드를 고쳐보자 - Setter들 대신에 생성자에서 인자로 초기화 - 객체를 변경하지 않는 멤버함수들은 const로 • 다양한 분산 빅데이터 알고리즘들을 조사해보자 - Immutability changes everything - http://queue.acm.org/detail.cfm?id=2884038 • 영속형 자료 구조에 대해서 알아보자 FP 실천사항 #2 - 불변객체
  • 16.
  • 17.
    고차함수 • 함수의 인자로한 개 이상의 함수를 넘겨 받거나, 함수의 결과값으로 다른 함수를 돌려주는 함수 • 고차함수의 보물창고 - #include <algorithm>
  • 18.
    FP의 기본 초식3총사 • std::transform • std::remove_if / std::copy_if • std::accumulate
  • 19.
    std::accumulate using Strings =std::vector<std::string>; int64_t hash(const std::string& str, int64_t seed); int64_t calculate_hash_by_stl(const Strings& strs) { return std::accumulate(std::begin(strs), std::end(strs), 12345ll, [](int64_t seed, const std::string& s) { return hash(s, seed); }); } 추천 동영상 – CppCon 2016 "std::accumulate: Exploring an Algorithmic Empire“ - https://youtu.be/B6twozNPUoA
  • 20.
    Unix 명령어들의 합성- PIPE history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head 47 ls 26 cd 19 sudo 17 clang++ 10 clang 7 history 7 ./a.out 5 ./compose 4 gedit 2 top
  • 21.
    예제 – 몬스터에게범위공격 • 특정 거리 r 이상 R 이하에 있는 대상을 공격 • 방어막이 있으면 방어막의 세기만큼 피해 감소 Dev-art by TH. Ahn
  • 22.
    Monster class class Monster{ public: Monster(); void setId(int id); void setHP(int hp); void setArmor(int armor); void setLocation(const Point& pt); int getHP() const; int getId() const; bool getArmor() const; Point getLocation() const; void gotDamage(int damage); private: int id_; int hp_; int armor_; Point pt_; }; using MonsterPtr = std::shared_ptr<Monster>; using Monsters = std::vector<MonsterPtr>; void Monster::gotDamage(int damage) { hp_ = hp_ - std::min(hp_, damage - armor_); } int calc_distance(const Point& pt1, const Point& pt2);
  • 23.
    Monsters doSplashDamage(const Point&my_location, const Monsters& monsters, int damage, int range_from, int range_to) { for (auto i = monsters.begin(); i != monsters.end(); ++i) { auto pt = *i; Monster& target = *pt; int distance = calc_distance(my_location, target.getLocation()); if (distance >= range_from && distance <= range_to) { target.gotDamage(damage); } } Monsters alive_monsters; for (auto i = monsters.begin(); i != monsters.end(); ++i) { auto pt = *i; Monster& target = *pt; if (target.getHP() > 0) { alive_monsters.push_back(pt); } } return alive_monsters; } 전형적인 절차형 코드
  • 24.
    우리가 하고 싶은것들 • 객체를 불변데이터로 다루고 싶다 • 가능한 순수함수를 쓰고 싶다 • 알고리즘 구현 부분과 사용 부분을 코드상에서 분리하고 싶다 • 임시변수들을 없애고 싶다 • 함수들을 쉽게 합성하고 싶다
  • 25.
    불변 Monster class와피해계산 순수함수 class Monster { public: Monster(int id, int hp, int armor, const Point& pt); int getHP() const; int getId() const; int getArmor() const; Point getLocation() const; static int calc_damaged_hp(int hp, int damage, int armor); private: int id_; int hp_; int armor_; Point pt_; }; int Monster::calc_damaged_hp(int hp, int damage, int armor) { return hp - std::min(hp, damage - armor); }
  • 26.
    주요 문장들을 함수로쪼개기 bool is_in_range(const Point& my_location, int range_from, int range_to, const MonsterPtr& pm); MonsterPtr damaged_monster(int damage, const MonsterPtr& pm); bool is_alive(const MonsterPtr& pm);
  • 27.
    Partial application bool is_in_range_(constPoint& my_location, int range_from, int range_to, const MonsterPtr& pm); bool partially_applied_is_in_range_(ANY_FIXED_LOC, ANY_FIXED_r, ANY_FIXED_R, const MonsterPtr& pm); bool still_a_function_with_only_one_parameter(const MonsterPtr& pm);
  • 28.
    Lambda로 Partial application1/2 auto is_in_range_(const Point& my_location, int range_from, int range_to) { return [my_location, range_from, range_to](const MonsterPtr& pm) { int distance = calc_distance(my_location, pm->getLocation()); return distance >= range_from && distance <= range_to; }; } auto damaged_monster_(int damage) { return [damage](const MonsterPtr& pm) { return std::make_shared<Monster>(pm->getId(), Monster::calc_damaged_hp(pm->getHP(), damage, pm->getArmor()), pm->getArmor(), pm->getLocation()); }; }
  • 29.
    Lambda로 Partial application2/2 auto do_damage_(const Point& my_location, int damage,int range_from, int range_to) { return [=](const MonsterPtr& pm) { return is_in_range_(my_location, range_from, range_to)(pm) ? damaged_monster_(damage)(pm) : pm; }; } bool is_alive(const MonsterPtr& pm) { return pm->getHP() > 0; }
  • 30.
    STL algorithm 이용하기 voiddoSplashDamage_stl(const Point& my_location, Monsters& monsters, int damage, int range_from, int range_to) { std::transform(monsters.begin(), monsters.end(), monsters.begin(), do_damage_(my_location, damage, range_from, range_to)); auto is_dead = [](const auto& m) { return !is_alive(m); }; monsters.erase(std::remove_if(monsters.begin(), monsters.end(), is_dead), monsters.end()); } “누가 네 줄로 짤 코드를 열줄로 만들어 놨어요?”
  • 31.
  • 32.
    Most effective STLusing ranges std::vector<int> numbers = { 1, 2, 3, 4, 5, 6, 7 }; std::vector<int> output; auto is_even = [](int i) { return i % 2 == 0; }; auto mul_2 = [](int i) { return i * 2; }; std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(output), is_even); std::transform(output.begin(), output.end(), output.begin(), mul_2); int sum = std::accumulate(output.begin(), output.end(), 0); auto rng = numbers | ranges::view::filter(is_even) | ranges::view::transform(mul_2); int sum = ranges::accumulate(rng, 0); // numbers is still {1, 2, 3, 4, 5, 6, 7};
  • 33.
    ranges::view로 algorithm을 합성하기 autodoSplashDamage_one_liner(const Point& my_location, const Monsters& monsters, int damage, int range_from, int range_to) { return monsters | ranges::view::transform(do_damage_(my_location, damage, range_from, range_to)) | ranges::view::filter(is_alive); } “누가 한 줄로 짤 코드를 열줄로 만들어 놨어요?”
  • 34.
    합성으로 구현한 코드의손쉬운 변경 auto doSplashDamage_one_liner(const Point& my_location, const Monsters& monsters, int damage, int range_from, int range_to) { return monsters | ranges::view::transform(do_damage_(my_location, damage, range_from, range_to)) | ranges::view::transform(apply_healing_(hp, range_from, range_to)) | ranges::view::filter(is_alive); } “기존의 코드는 건드리지 조차 않아도 되니 안심!”
  • 35.
    ranges의 지연연산 auto doSplashDamage_top_3(constPoint& my_location, const Monsters& monsters, int damage, int range_from, int range_to) { return monsters | ranges::view::transform(do_damage_(my_location, damage, range_from, range_to)) | ranges::view::filter(is_alive) | ranges::view::take(3); } 쓰지 않을 객체는 만들지도 말고, 필요 없는 계산은 하지도 말자!
  • 36.
    std::ranges가 되고 싶은Ranges TS • STL을 더욱 효과적으로 활용 • 불변 컨테이너 • 합성 / 지연연산 / Monad * • https://github.com/ericniebler/range-v3 • https://github.com/Microsoft/Range-V3-VS2015
  • 37.
    • 고차함수를 사용해서코드를 간결하게 만들자 - 간결한 코드 -> 가독성 -> 적은 버그 • 알고리즘의 구현 부분과 사용 부분을 분리하자 - 로직 구현에 실제로 관련 없는 임시변수와 상태들을 없애자 • 함수를 합성 가능하게 만들어서 재활용성을 높이자 - 검증된 재활용이 쉬운 Building block들을 합성 • 지연연산을 활용해서 최적화를 시도하자 - 무한리스트에 대해서 알아보자 FP 실천사항 #3 – 고차함수와 합성
  • 38.
  • 39.
    타입의 활용 • 메모리상에저장된 데이터가 쓰일 방법을 정의 • 가능한 빠른 단계에서 오류 발견 • 코드의 의미 함축성 증가 • 코드 작성중의 힌트 • 로직 설계의 도구 • 지원하지 않는 동작은 표현 자체가 불가능 하도록 • 합성 동작의 연결을 위한 접착제
  • 40.
    std::optional in C++17 voidoptional_demo() { std::optional<std::string> name; assert(!name); if (name) std::cout << *name << std::endl; std::optional<std::string> a_name("Anonymous"); assert(a_name); assert(a_name.value() == "Anonymous"); assert(*a_name == "Anonymous"); if (a_name) std::cout << *a_name << std::endl; } • null – “The worst mistake of computer science” • Type safe / no dynamic allocation
  • 41.
    std::variant in C++17 std::variant<std::string,bool, int> v = std::string("Hello!"); assert(std::get<std::string>(v) == "Hello!"); std::variant<std::string, bool, int> b = true; assert(std::get<bool>(b) == true); try { std::cout << std::boolalpha << std::get<bool>(v) << std::endl; } catch (std::bad_variant_access&) { std::cout << "exception..." << std::endl; } • Discriminated union • Never-empty guarantee • Type safe / no dynamic allocation
  • 42.
    Quiz #1 struct Human{ unsigned char age_; bool alive_; }; 다음 타입은 몇 개의 서로 다른 값(상태)를 나타낼 수 있을까요? 256 * 2 = 512 using A_Tuple_Type = std::tuple<unsigned char, bool>;
  • 43.
    Quiz #2 enum Gender: int { MALE, FEMALE, UNKNOWN }; struct Human { Gender gender_; unsigned char age_; bool alive_; }; Human은 몇 개의 서로 다른 값(상태)를 나타낼 수 있을까요? 3 * 256 * 2 = 1536
  • 44.
    Quiz #3 using Another_Tuple_Type= std::tuple<Gender, unsigned char, bool>; 다음 타입은 몇 개의 서로 다른 값(상태)를 나타낼 수 있을까요? 3 * 256 * 2 = 1536
  • 45.
    동등한 타입 struct Human{ Gender gender_; unsigned char age_; bool alive_; }; std::tuple<Gender, unsigned char, bool>;
  • 46.
    Quiz #4 std::optional<Human>; 다음 타입은몇 개의 서로 다른 값(상태)를 나타낼 수 있을까요? 1536 + 1 = 1537
  • 47.
    Quiz #5 struct Alien{ bool alive_; }; std::variant<Human, Alien>; 다음 타입은 몇 개의 서로 다른 값(상태)를 나타낼 수 있을까요? 1536 + 2 = 1538
  • 48.
    대수적 자료형 -Algebraic data types (ADTs) in C++ • 타입도 합성 • Product types (*) – std::pair, std::tuple, struct/class • Sum types (+) – std::optional, std::variant
  • 49.
    ADTs로 표현 한다면?– 타입의 합성 class HttpRequest { //... private: Url url_; SecureUrl url_ssl_; bool ssl_; std::string id_; std::string password_; bool need_auth_; }; class HttpRequest { //... private: std::variant<Url, SecureUrl> url_; using Credential = std::tuple<std::string, std::string>; std::optional<Credential> auth_; }; • 유효한 상태의 개수보다 데이터 타입의 Cardinality가 크다면? 무효한 상태를 가질 가능성 -> 버그!
  • 50.
    • 타입을 최대한사용해서 안전한 코드를 만들자 - 불가능한 동작은 표현 자체가 불가능하도록 • 타입이 표현가능한 상태의 개수(Cardinality)를 계산해보자 - Product 타입, Sum 타입 • 유효하지 않은 상태를 가질 수 없는 타입을 정의하자 FP 실천사항 #4 – 타입 시스템
  • 51.
  • 52.
    Download a fileand decompress it std::tuple<int, std::string> download(const std::string& url); bool verify(const std::string& body); bool decompress(const std::string& body, std::string& uncompressed); std::string imperative(const std::string& url) { auto r = download(url); int status_code = std::get<int>(r); const std::string& body = std::get<std::string>(r); if (status_code == 200) { bool verified = verify(body); if (verified) { std::string decompressed; bool success = decompress(body, decompressed); if (success) { return decompressed; } } } return std::string(); }
  • 53.
    std::optional 을 사용하면 std::optional<std::string>download_fn(const std::string& url) { auto r = download(url); int status_code = std::get<int>(r); const std::string& body = std::get<std::string>(r); return status_code == 200 ? std::make_optional(body) : std::nullopt; } std::optional<std::string> verify_fn(const std::string& body) { bool verified = verify(body); return verified ? std::make_optional(body) : std::nullopt; } std::optional<std::string> decompress_fn(const std::string& body) { std::string decompressed; bool success = decompress(body, decompressed); return success ? std::make_optional(decompressed) : std::nullopt; }
  • 54.
    조금 단순화된 ifelse 문들 std::string do_download(const std::string& url) { auto body = download_fn(url); if (body) { auto verified = verify_fn(*body); if (verified) { auto decompressed = decompress_fn(*verified); if (decompressed) return *decompressed; } } return std::string(); }
  • 55.
    template <typename A> std::optional<A>unit_(A a) { return std::make_optional(a); } template <typename F> std::optional<std::string> bind_(const std::optional<std::string>& v, F func) { return v ? func(*v) : std::nullopt; } Define two functions • 이 두개의 함수를 이용해서 표준적인 방법으로 입출력 타입이 호환되는 함수들을 합성하자!
  • 56.
    Using Maybe Monad voidmonad_demo() { auto download_ = [](const std::string& url) { auto r = download(url); int status_code = std::get<int>(r); const std::string& body = std::get<std::string>(r); return status_code == 200 ? unit_(body) : std::nullopt; }; auto verify_ = [](const std::string& body) { bool verified = verify(body); return verified ? unit_(body) : std::nullopt; }; auto decompress_ = [](const std::string& body) { std::string decompressed; bool success = decompress(body, decompressed); return success ? unit_(decompressed) : std::nullopt; }; auto m = [=](auto v) { return bind_(bind_(bind_(unit_(v), download_), verify_), decompress_); }; auto result = m(std::string("http://ndc.nexon.com/files/testdata.txt")); if (result) std::cout << *result << std::endl; }
  • 57.
    auto m =[=](auto v) { return bind_(bind_(bind_(unit_(v), download_), verify_), decompress_); }; 마지막으로 연산자를 오버로딩 하면 • ranges::view 들을 합성하던 것과 같은 방법! auto m = [=](auto v) { return v | download_ | verify_ | decompress_; };
  • 58.
    Monad A monad iscreated by defining a type constructor M and two operations, bind and return (where return is often also called unit): • The unary return operation takes a value from a plain type (a) and puts it into a container using the constructor, creating a monadic value (with type M a). • The binary bind operation ">>=" takes as its arguments a monadic value with type M a and a function (a → M b) that can transform the value. • The bind operator unwraps the plain value with type a embedded in its input monadic value with type M a, and feeds it to the function. • The function then creates a new monadic value, with type M b, that can be fed to the next bind operators composed in the pipeline. https://en.wikipedia.org/wiki/Monad_(functional_programming)
  • 59.
    Monad in C++ •Maybe monad using std::optional • Ranges TS - std::ranges in C++20 • Composable std::future in C++20
  • 60.
    요점정리 • 상태가 필수적인경우가 아니라면 가능한 순수함수를 만들자 • 불변데이터와 관련 알고리즘들을 이해하고 활용하자 • 고차함수를 사용해서 코드를 간결하게 하자 • 알고리즘의 구현과 사용 부분을 코드상에서 분리하자 • 타입을 적극 활용해서 안전한 코드를 만들자 • 합성을 사용해서 코드의 변경을 쉽게 하고 재활용성을 높이자 • 함수형 프로그래밍 언어를 한가지 공부해보자 - Haskell 강추!
  • 61.