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; // Alias
auto d = Distance{3} + 2; // Ok: just double
using 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 {}; // struct
template<class...> struct OneOf {}; // variant
template<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
ADL rettet uns!
// Storage with ADL
namespace 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 values
template<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 OrderedTree
template<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 OrderedTree
template<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<>; // Fallback
template<class T> using ToComputedValues = decltype(toComputedValues(std::declval<T>()));
// Schema -> Computed Schema
template<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);
}