Type driven Development

mit C++

ADC++, Regensburg, Mai 2019

17

Andreas Reischuck

Was ist ein Typ?

Was ist ein Typ?

…für den Prozessor

Was ist ein Typ?

…für den Compiler

Was ist ein Typ?

…für den Entwickler

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{};
};
BarneyDellar StrongTypes CppOnSea

Strong Types in C - Barney Dellar [C on Sea 2019]

VictorCiura RegularTypes Accu2019

Regular Types and Why Do I Care ? - Victor Ciura [ACCU 2019]

Anwendungsfall

Aktion Kommando Server Events Berechnungen Updates Ansichten

Datenschema

Beispiele

Geburtstag

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

Einfache Speicherung

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.

cppreference.com

ADL rettet uns!

(in other words, adding a new function declaration after template definition does not make it visible except via ADL)

cppreference.com

// 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

// 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

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

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

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

Was noch?

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

Zusammenfassung

++ Vorteile ++  

-- Nachteile --

Anwendungsszenarien

Links

andreas

 

hicknhackLogo new text

 

Work with us…

cppug

 

Give a Talk
⇒ get a Dresden tour

rebuild logo

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);
}