Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

New Tools for a More Functional C++

11,603 views

Published on

Variants have been around in C++ for a long time and C++17 now has std::variant. We will compare inheritance and std::variant for their ability to model sum-types (a fancy name for tagged unions). We will visit std::visit and discuss how it helps us model the pattern matching idiom. Immutability is one of the core pillars of Functional Programming (FP). C++ now allows you to model deep immutability; we'll see a way to do that using the standard library. We'll explore if `return std::move(*this)` makes any sense in C++. Immutability may be a reason for that.

Published in: Software

New Tools for a More Functional C++

  1. 1. New Tools for a More Functional C++ Sumant Tambe Sr. Software Engineer, LinkedIn Microsoft MVP SF Bay Area ACCU Sept 28, 2017
  2. 2. Blogger (since 2005) Coditation—Elegant Code for Big Data Author (wikibook) Open-source contributor Since 2013 (Visual Studio and Dev Tech)
  3. 3. Functional Programming in C++ by Ivan Cukic (ETA early 2018) Reviewer
  4. 4. Sum Types and (pseudo) Pattern Matching
  5. 5. Modeling Alternatives Inheritance vs std::variant • States of a game of Tennis • NormalScore • DeuceScore • AdvantageScore • GameCompleteScore
  6. 6. Modeling game states using std::variant struct NormalScore { Player p1, p2; int p1_score, p2_score; }; struct DeuceScore { Player p1, p2; }; struct AdvantageScore { Player lead, lagging; }; struct GameCompleteScore { Player winner, loser; int loser_score; }; using GameState = std::variant<NormalScore, DeuceScore, AdvantageScore, GameCompleteScore>;
  7. 7. Print GameState (std::variant) struct GameStatePrinter { std::ostream &o; explicit GameStatePrinter(std::ostream& out) : o(out) {} void operator ()(const NormalScore& ns) const { o << "NormalScore[" << ns.p1 << ns.p2 << ns.p1_score << ns.p2_score << "]"; } void operator ()(const DeuceScore& ds) const { o << "DeuceScore[" << ds.p1 << "," << ds.p2 << "]"; } void operator ()(const AdvantageScore& as) const { o << "AdvantageScore[" << as.lead << "," << as.lagging << "]"; } void operator ()(const GameCompleteScore& gc) const { o << "GameComplete[" << gc.winner << gc.loser << gc.loser_score << "]"; } }; std::ostream & operator << (std::ostream& o, const GameState& game) { std::visit(GameStatePrinter(o), game); return o; }
  8. 8. std::visit spews blood when you miss a case
  9. 9. Print GameState. Fancier! std::ostream & operator << (std::ostream& o, const GameState& game) { std::visit(overloaded { [&](const NormalScore& ns) { o << "NormalScore" << ns.p1 << ns.p2 << ns.p1_score << ns.p2_score; }, [&](const DeuceScore& gc) { o << "DeuceScore[" << ds.p1 << "," << ds.p2 << "]"; }, [&](const AdvantageScore& as) { o << "AdvantageScore[" << as.lead << "," << as.lagging << "]"; }, [&](const GameCompleteScore& gc) { o << "GameComplete[" << gc.winner << gc.loser << gc.loser_score << "]"; } }, game); return o; }
  10. 10. Passing Two Variants to std::visit std::ostream & operator << (std::ostream& o, const GameState& game) { std::visit(overloaded { [&](const NormalScore& ns, const auto& other) { o << "NormalScore" << ns.p1 << ns.p2 << ns.p1_score << ns.p2_score; }, [&](const DeuceScore& gc, const auto& other) { o << "DeuceScore[" << ds.p1 << "," << ds.p2 << "]"; }, [&](const AdvantageScore& as, const auto& other) { o << "AdvantageScore[" << as.lead << "," << as.lagging << "]"; }, [&](const GameCompleteScore& gc, const auto& other) { o << "GameComplete[" << gc.winner << gc.loser << gc.loser_score << "]"; } }, game, someother_variant); return o; } There can be arbitrary number of arbitrary variant types. The visitor must cover all cases
  11. 11. A Template to Inherit Lambdas template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; explicit overloaded(Ts... ts) : Ts(ts)... {} }; A User-Defined Deduction Guide explicit overloaded(Ts... ts) : Ts(ts)... {} template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
  12. 12. Next GameState Algorithm (all cases in one place) GameState next (const GameState& now, const Player& who_scored) { return std::visit(overloaded { [&](const DeuceScore& ds) -> GameState { if (ds.p1 == who_scored) return AdvantageScore{ds.p1, ds.p2}; else return AdvantageScore{ds.p2, ds.p1}; }, [&](const AdvantageScore& as) -> GameState { if (as.lead == who_scored) return GameCompleteScore{as.lead, as.lagging, 40}; else return DeuceScore{as.lead, as.lagging}; }, [&](const GameCompleteScore &) -> GameState { throw "Illegal State"; }, [&](const NormalScore& ns) -> GameState { if (ns.p1 == who_scored) { switch (ns.p1_score) { case 0: return NormalScore{ns.p1, ns.p2, 15, ns.p2_score}; case 15: return NormalScore{ns.p1, ns.p2, 30, ns.p2_score}; case 30: if (ns.p2_score < 40) return NormalScore{ns.p1, ns.p2, 40, ns.p2_score}; else return DeuceScore{ns.p1, ns.p2}; case 40: return GameCompleteScore{ns.p1, ns.p2, ns.p2_score}; default: throw "Makes no sense!"; } } else { switch (ns.p2_score) { case 0: return NormalScore{ns.p1, ns.p2, ns.p1_score, 15}; case 15: return NormalScore{ns.p1, ns.p2, ns.p1_score, 30}; case 30: if (ns.p1_score < 40) return NormalScore{ns.p1, ns.p2, ns.p1_score, 40}; else return DeuceScore{ns.p1, ns.p2}; case 40: return GameCompleteScore{ns.p2, ns.p1, ns.p1_score}; default: throw "Makes no sense!"; } } } }, now); }
  13. 13. Modeling Alternatives with Inheritance class GameState { std::unique_ptr<GameStateImpl> _state; public: void next(const Player& who_scored) {} }; class GameStateImpl { Player p1, p2; public: virtual GameStateImpl * next( const Player& who_scored) = 0; virtual ~GameStateImpl(){} }; class NormalScore : public GameStateImpl { int p1_score, p2_score; public: GameStateImpl * next(const Player&); }; class DeuceScore : public GameStateImpl { public: GameStateImpl * next(const Player&); }; class AdvantageScore : public GameStateImpl { int lead; public: GameStateImpl * next(const Player&); }; class GameCompleteScore :public GameStateImpl{ int winner, loser_score; public: GameStateImpl * next(const Player&); };
  14. 14. Sharing State is easier with Inheritance class GameState { std::unique_ptr<GameStateImpl> _state; public: void next(const Player& who_scored) {} Player& who_is_serving() const; double fastest_serve_speed() const; GameState get_last_state() const; }; class GameStateImpl { Player p1, p2; int serving_player; double speed; GameState last_state; public: virtual GameStateImpl * next( const Player& who_scored) = 0; virtual Player& who_is_serving() const; virtual double fastest_serve_speed() const; virtual GameState get_last_state() const; };
  15. 15. Sharing Common State is Repetitive with std::variant Player who_is_serving = std::visit([](auto& s) { return s.who_is_serving(); }, state); Player who_is_serving = state.who_is_serving(); ceremony! struct NormalScore { Player p1, p2; int p1_score, p2_score; int serving_player; Player & who_is_serving(); }; struct DeuceScore { Player p1, p2; int serving_player; Player & who_is_serving(); }; struct AdvantageScore { Player lead, lagging; int serving_player; Player & who_is_serving(); }; struct GameCompleteScore { Player winner, loser; int loser_score; int serving_player; Player & who_is_serving(); };
  16. 16. How about recursive std::variant? struct NormalScore { Player p1, p2; int p1_score, p2_score; int serving_player; Player & who_is_serving(); GameState last_state; }; struct DeuceScore { Player p1, p2; int serving_player; Player & who_is_serving(); GameState last_state; }; struct AdvantageScore { Player lead, lagging; int serving_player; Player & who_is_serving(); GameState last_state; }; struct GameCompleteScore { Player winner, loser; int loser_score; int serving_player; Player & who_is_serving(); GameState last_state; }; Not possible unless you use recursive_wrapper and dynamic allocation. Not in C++17. Dare I say, it’s not algebraic? It does not compose  std::variant is a container. Not an abstraction.
  17. 17. std::variant disables fluent interfaces { using GameState = std::variant<NormalScore, DeuceScore, AdvantageScore, GameCompleteScore>; GameState state = NormalScore {..}; GameState last_state = std::visit([](auto& s) { return s.get_last_state(); }, state); double last_speed = std::visit([](auto& s) { return state.fastest_serve_speed(); }, last_state); double last_speed = state.get_last_state().fastest_serve_speed(); } ceremony!
  18. 18. Combine Implementation Inheritance with std::variant { using GameState = std::variant<NormalScore_v2, DeuceScore_v2, AdvantageScore_v2, GameCompleteScore_v2>; GameState state = NormalScore_v2 {..}; Player who_is_serving = std::visit([](SharedGameState& s) { return s.who_is_serving(); }, state); Player who_is_serving = state.who_is_serving(); } SharedGameState who_is_serving() NormalScore_v2 DeuceScore_v2 AdvantageScore_v2 GameCompleteScore_v2 ceremony!
  19. 19. Modeling Alternatives Inheritance std::variant Dynamic Allocation No dynamic allocation Intrusive Non-intrusive Reference semantics (how will you copy a vector?) Value semantics Algorithm scattered into classes Algorithm in one place Language supported Clear errors if pure-virtual is not implemented Library supported std::visit spews blood on missing cases Creates a first-class abstraction It’s just a container Keeps fluent interfaces Disables fluent interfaces. Repeated std::visit Supports recursive types (Composite) Must use recursive_wrapper and dynamic allocation. Not in the C++17 standard.
  20. 20. Deep Immutability
  21. 21. const is shallow struct X { void bar(); }; struct Y { X* xptr; explicit Y(X* x) : xptr(x) {} void foo() const { xptr->bar(); } }; { const Y y(new X); y.foo(); // mutates X?? }
  22. 22. Deep Immutability: propagate_const<T> struct X { void bar(); void bar() const; // Compiler error without this function }; struct Y { X* xptr; propagate_const<X *> xptr; explicit Y(X* x) : xptr(x) {} void foo() const { xptr->bar(); } }; { const Y y(new X); y.foo(); // calls X.bar() const }
  23. 23. Deep Immutability: propagate_const<T> #include <experimental/propagate_const> using std::experimental::propagate_const; { propagate_const<X *> xptr; propagate_const<std::unique_ptr<X>> uptr; propagate_const<std::shared_ptr<X>> shptr; const propagate_const<std::shared_ptr<X>> c_shptr; shptr.get() === X* c_shptr.get() === const X* *shptr === X& *c_shptr === const X& get_underlying(shptr) === shared_ptr<X> get_underlying(c_shptr) === const shared_ptr<X> shptr = another_shptr; // Error. Not copy-assignable shptr = std::move(another_shptr) // but movable Library fundamental TS v2
  24. 24. Mutable Temporaries
  25. 25. The Named Parameter Idiom (mutable) class configs { std::string server; std::string protocol; public: configs & set_server(const std::string& s); configs & set_protocol(const std::string& s); }; start_server(configs().set_server(“localhost”) .set_protocol(“https”));
  26. 26. The Named Parameter Idiom (immutable) class configs { public: configs set_server(const std::string& s) const { configs temp(*this); temp.server = s; return temp; } configs set_protocol(const std::string& proto) const { configs temp(*this); temp.protocol = proto; return temp; } }; start_server(configs().set_server(“localhost”) .set_protocol(“https”)); Avoid copy-constructors?
  27. 27. The Named Parameter Idiom (immutable*) class configs { public: configs set_server(const std::string& s) const { configs temp(*this); temp.server = s; return temp; } configs set_protocol(const std::string& proto) const { configs temp(*this); temp.protocol = proto; return temp; } configs set_server(const std::string& s) && { server = s; return *this; } configs set_protocol(const std::string& proto) && { protocol = proto; return *this; } }; start_server(configs().set_server(“localhost”) .set_protocol(“https”)); & &
  28. 28. The Named Parameter Idiom (immutable*) class configs { public: configs set_server(const std::string& s) const { configs temp(*this); temp.server = s; return temp; } configs set_protocol(const std::string& proto) const { configs temp(*this); temp.protocol = proto; return temp; } configs&& set_server(const std::string& s) && { server = s; return *this; std::move(*this); } configs&& set_protocol(const std::string& proto) && { protocol = proto; return *this; std::move(*this); } }; start_server(configs().set_server(“localhost”) .set_protocol(“https”)); & &
  29. 29. Thank You!

×