Type driven Development
mit C++
ADC++, Regensburg, Mai 2019
Was ist ein Typ?
Was ist ein Typ?
…für den Prozessor
- Interpretation von Bits
- Recheneinheiten
- Alignment
Was ist ein Typ?
…für den Compiler
- Aggregate
- Anzahl Bytes
- Lifecycle
Was ist ein Typ?
…für den Entwickler
- Ordnung
- Semantik
- Funktionen
Starke Typen
Strong Types
using Distance = double; // Aliasauto d = Distance{3} + 2; // Ok: just doubleusing Velocity = Strong<double, struct VelocityTag>;auto v = Velocity{3} + 2; // Error: no operator
template<class V, class... /*Tags*/> struct Strong {V v{};};
Regular Types and Why Do I Care ? - Victor Ciura [ACCU 2019]
Anwendungsfall
- verteiltes System
- (vereinfacht)
Datenschema
Beispiele
- XML / JSON-Schema
- Tabellenschema (Datenbanken)
- Objektbeziehungsmodell (ERM)
Schema mit C++
// schema primitives:template<class...> struct AllOf {}; // structtemplate<class...> struct OneOf {}; // varianttemplate<class...> struct SomeOf {};template<class Id, class> struct EntitySet {};template<class Id, class> struct IdMap {};// …template<class Id, class Node, class Leaf> struct OrderedTree {};
// example usage:enum class Anrede { Neutral, Herr, Frau };using PersonId = Strong<int, struct PersonIdTag>;using Vorname = Strong<string, struct VornameTag>;using Nachname = Strong<string, struct NachnameTag>;using PersonData = AllOf<Anrede, Vorname, Nachname>;using Persons = EntitySet<PersonId, PersonData>;
using Command = ToCommand<Persons>;using Repository = ToRepository<Persons>;constexpr auto processCommand = to_command_processor<Persons>;void testCreate() {auto repo = Repository{};using CreateCmd = EntityCreate<PersonData>;auto cmd = CreateCmd{ Anrede::Herr, Vorname{"Bjarne"}, Nachname{"Stroustrup"} };processCommand(cmd, repo);}
Typ getriebene Code Generierung
Ziele
ToCommand<T>- BefehleToRepository<T>- Repositoryto_command_processor<T>- Verarbeitung- Netzwerk-Protokolle, Gui, …
Einfache Speicherung
ToStorage<T>
template<class T> auto toStorage(T);template<class T> using ToStorage = decltype(toStorage(T));template<class... Ts> auto toStorage(AllOf<Ts...>) -> std::tuple<ToStorage<Ts>...>;using Test = ToStorage<AllOf<>>; // error
Reihenfolge-Problem
…, unqualified name lookup takes place when the template definition is examined.
ADL rettet uns!
(in other words, adding a new function declaration after template definition does not make it visible except via ADL)
// Storage with ADLnamespace storage {template<class T> struct ADL {};template<class T> using ToStorage = decltype(toStorage(ADL<T>{}));template<class... Ts> auto toStorage(ADL<AllOf<Ts...>>) -> std::tuple<ToStorage<Ts>...>;// …} // namespace storage
template<class... Ts> auto toStorage(ADL<AllOf<Ts...>>) -> std::tuple<ToStorage<Ts>...>;template<class... Ts> auto toStorage(ADL<OneOf<Ts...>>)-> std::variant<ToStorage<Ts>...>;template<class Id, class Data> auto toStorage(ADL<EntitySet<Id, Data>>)-> std::vector<std::tuple<Id, ToStorage<Data>>>;
Pattern
- Je abstraktem Typ
- Eine Spezialisierung
- Rekursion für Untertypen
// storage for valuestemplate<class T> constexpr bool isValue() {if constexpr (std::is_class_v<T>) return !std::is_empty_v<T>;else return std::is_enum_v<T>;}template<class T> auto toStorage(ADL<T>) -> std::enable_if_t<isValue<T>(), T>;
// Storage for OrderedTreetemplate<class Id> using ParentId = StrongAddTag<Id, struct ParentIdTag>;template<class Id, class Node, class Leaf> using TreeNode = std::tuple<Id, ParentId<Id>,std::variant<ToStorage<Node>, ToStorage<Leaf>>>;template<class Id, class Node, class Leaf> auto toStorage(ADL<OrderedTree<Id, Node, Leaf>>) -> std::vector<TreeNode<Id, Node, Leaf>>;
Befehle
- ✔
ToStorage<T> ToCommand<T>
template<class Data> using EntityCreate = ToStorage<Data>;template<class Id, class Data> using EntityUpdate = std::tuple<Id, ToCommand<Data>>;template<class Id> using EntityDelete = Id;template<class Id, class Data> auto toCommand(ADL<EntitySet<Id, Data>>)-> std::variant<EntityCreate<Data>,EntityUpdate<Id, Data>,EntityDelete<Id>>;
// Commands for OrderedTreetemplate<class Id, class Node, class Leaf> using TreeCreate = std::tuple<ParentId<Id>, BeforeId<Id>, ToStorage<OrderedTree<Id, Node, Leaf>>>;template<class Id, class Node, class Leaf> using TreeUpdate = std::tuple<Id, std::variant<ToCommand<Node>, ToCommand<Leaf>>>;template<class Id> using TreeMove = std::tuple<Id, ParentId<Id>, BeforeId<Id>>;template<class Id> using TreeDelete = Id;template<class Id, class Node, class Leaf> auto toCommand(ADL<OrderedTree<Id, Node, Leaf>>)-> std::variant<TreeCreate<Id, Node, Leaf>,TreeUpdate<Id, Node, Leaf>,TreeMove<Id>, TreeDelete<Id>>;
Repository
- ✔
ToStorage<T> - ✔
ToCommand<T> ToRepository<T>
template<class Id, class Data> auto toRepository(ADL<EntitySet<Id, Data>>)-> std::map<Id, ToRepository<Data>>;
template<class Id, class Data> class EntityRepository { std::map<Id, ToRepository<Data>> m; public:auto operator[] (Id) -> ToRepository<Data>&;void create(const ToStorage<Data>&);void remove(Id);};template<class Id, class Data> auto toRepository(ADL<EntitySet<Id, Data>>)-> EntityRepository<Id, Data>;
Befehlsverarbeitung
- ✔
ToStorage<T> - ✔
ToCommand<T> - ✔
ToRepository<T> to_command_processor<T>
Command ∘ Repository → Updated Repository
// Processor Boilerplate namespace processor {template<class T> struct ADL {};template<class T> auto toCommandProcessor(T); // Lambda(cmd, repo&)template<class T> constexpr auto to_command_processor = toCommandProcessor(ADL<T>{});} // namespace processor
template<class Id, class Data> constexpr auto toCommandProcessor(ADL<EntitySet<Id, Data>>) {return [](const ToCommand<EntitySet<Id, Data>>& cmd, ToRepository<EntitySet<Id, Data>>& repo) {oneVisit(cmd,[&repo](const ToStorage<Data>& storage) { repo.create(storage); },[&repo](const std::tuple<Id, ToCommand<Data>>& update) { auto [id, dataCmd] = update; to_command_processor<Data>(dataCmd, repo[id]); },[&repo](Id id) { repo.remove(id); });};}
Zwischenstand
- ✔
ToStorage<T> - ✔
ToCommand<T> - ✔
ToRepository<T> - ✔
to_command_processor<T>
Was noch?
- Netzwerk-Protokolle
- Berechnungen
- Gui
Berechnungen
using Ansprache = Strong<std::string, struct AnspracheTag>;auto toComputedValues(PersonData) -> AllOf<Ansprache>;void compute(const ToStorage<PersonData>& s, Ansprache& o) { auto anrede = std::get<Anrede>(s); auto& nachname = std::get<Nachname>(s);auto out = std::stringstream{}; switch (anrede) { case Anrede::Neutral: out << "Hallo " << nachname.v; break; case Anrede::Herr: out << "Sehr geehrter Herr " << nachname.v; break; case Anrede::Frau: out << "Sehr geehrte Frau " << nachname.v; break; } o.v = out.str();}
void testCompute() {auto input = EntityCreate<PersonData>{Anrede::Herr, Vorname{"Bjarne"}, Nachname{"Stroustrup"}};auto output = Ansprache{};compute::compute(input, output);}
void testCompute() { auto input = EntityCreate<PersonData>{Anrede::Herr, Vorname{"Bjarne"}, Nachname{"Stroustrup"}}; auto output = Ansprache{}; compute::compute(input, output);using OutPersons = ToComputed<Persons>;using OutCommand = ToCommand<OutPersons>;using OutCreateCommand = EntityCreate<ToComputed<PersonData>>;using OutRepository = ToRepository<OutPersons>;constexpr auto processOutCommand = processor::to_command_processor<OutPersons>;OutCommand outCmd1 = OutCreateCommand{ std::get<Anrede>(input), std::get<Vorname>(input), std::get<Nachname>(input), output };OutRepository outRepo;processOutCommand(outCmd1, outRepo);}
template<class T> auto toComputedValues(T) -> AllOf<>; // Fallbacktemplate<class T> using ToComputedValues = decltype(toComputedValues(std::declval<T>()));// Schema -> Computed Schematemplate<class... Ts> auto toComputed(ADL<AllOf<Ts...>>)-> Join<AllOf<ToComputed<Ts>...>, ToComputedValues<AllOf<Ts...>>>;// … keep remaining schema
Demo
Qt - Gui
- Qt moc - Meta Object Compiler
- Woboq Verdigris
- … with internal APIs
Zusammenfassung
- Von einem Typ getriebenen Schema
- lässt sich fast alles ableiten.
++ Vorteile ++
- Zentrales Schema Definition
- Trennung von Logik und Daten
- Sehr gute Testbarkeit
-- Nachteile --
- Ungewohnt + Lernaufwand
- C++ benötigt Boilerplate
- lange Typennamen
Anwendungsszenarien
- komplexe verteilte Software
- Performancekritische Projekte
Links
- Andreas Reischuck
- @arBmind
Work with us…
Give a Talk
⇒ get a Dresden tour
Rebuild language project
Collaborate
Probiert mehr aus!
Probiert Typ-getriebene-Entwicklung!
Photo Credits
Danke!
co_await question_ready()
Opaque Strong Types
struct PersonId;constexpr auto makeOpaque(Strong<int, struct PersonIdTag>) -> PersonId;struct PersonId : Strong<int, struct PersonIdTag> {using Strong::Strong;};
#define STRONG_OPAQUE(name, type, ...) \
struct name; \
constexpr auto makeOpaqueType \
(Strong<type, ##__VA_ARGS__>) -> name; \
struct name : Strong<type, ##__VA_ARGS__> { \
using Strong::Strong; \
}
STRONG_OPAQUE(PersonId, int);Join Type Packs
template <class... As, class... Bs>
auto join(AllOf<As...>, AllOf<Bs...>)
-> AllOf<As...,Bs...>;
template<class A, class B>
using Join = decltype(
join(std::declval<A>(), std::declval<B>())
);OneVisit
template<class... Fs> struct Overloaded : Fs... { using Fs::operator()...; };template<class... Fs> Overloaded(Fs...) -> Overloaded<Fs...>;template<class V, class... Fs> auto oneVisit(V &&v, Fs &&... fs) {return std::visit(Overloaded{fs...}, v);}