JUANPE BOLÍVAR PRESENTS
http://sinusoid.al

Squaring the circle


value-oriented design in an object-oriented system

Part I

Circles and squares

“The limits of my language are the limits of my world.”

Ludwig Wittgenstein

using room_id = int;

struct room {
    string name;
    bool light_on;
};

struct house {
    vector<room> rooms;
    vector<unique_ptr<rooms>> rooms;
    vector<shared_ptr<rooms>> rooms;
    vector<shared_ptr<rooms>> rooms;
    vector<rooms> rooms;
    unordered_map<room_id, rooms> rooms;
    unordered_map<room_id, rooms> rooms;
    vector<pair<room*,room*>> doors;
    vector<pair<room*,room*>> doors;
    vector<pair<weak_ptr<room>, weak_ptr<room>>> doors;
    vector<pair<weak_ptr<room>, weak_ptr<room>>> doors;
    vector<pair<room_id, room_id>> doors;
    boost::multi_index<room_id,room_id> doors;
    boost::multi_index<room_id,room_id> doors;
};

Postmodern Immutable Data-Structures

Juanpe Bolívar, CppRussia Moscow '19

youtu.be/dbFfpTp3EhA

Part II

Modularity


struct room {
    string name;
    bool light_on;
    boost::signal<bool> on_light_on_changed;
};

struct house {
    vector<shared_ptr<rooms> rooms;
    vector<pair<weak_ptr<room>, weak_ptr<room>>> doors;
};

struct room_component
{
    Widget::Button button;

    room_component(std::shared_ptr<room> r, Widget::View& parent)
        : button{parent}
    {
        button.on_clicked.connect([r] {
            r->light_on = !r->light_on;
            r->on_light_on_changed(light_on);
        });
        auto update = [=] {
            button.SetText(r->light_on ? "Turn off!" : "Turn on!");
        };
        room_->on_light_on_changed.connect(update);
        update();
    }
};

The most valuable values

Juanpe Bolívar, CppCon'18

youtu.be/_oBx_NbLghY

Part III

Cursors


template <typename T>
struct state
{
    state(T init = T{})
       : node_{std::make_shared({std::move(init)})}
    {}
    const T& get() const {
        return node_->curr;
    }
    void set(T v) {
        using std::swap;
        swap(node_->curr, v);
        node_->on_change(v, node_->curr);
    }
    friend auto watch(state& s, std::function<const T&, const T&> cb) {
        return node_->on_change.connect(std::move(cb));
    }
private:
    struct node {
       T curr;
       boost::signal<const T&, const T&> on_change;
    };
    std::shared_ptr<node> node_;
};

struct room_component
{
    Widget::Button button;

    room_component(std::shared_ptr<room> r, Widget::View& parent)
        : button{parent}
    {
        button.on_clicked.connect([r] {
            r->light_on = !r->light_on;
            r->on_light_on_changed(light_on);
        });
        auto update = [=] {
            button.SetText(r->light_on ? "Turn off!" : "Turn on!");
        };
        room_->on_light_on_changed.connect(update);
        update();
    }
};


$😭$

struct room_component
{
    Widget::Button button;

    room_component(state<house> s, room_id id, Widget::View& parent)
        : button{parent}
    {
        button.on_clicked.connect([s] {
            auto h = s.get();
            h.rooms[id].light_on = !h.rooms[id].light_on;
            s.set(h);
        });
        auto update = [=] (auto&& h) {
            button.SetText(h.rooms[id].light_on ? "Turn off!" : "Turn on!");
        };
        watch(s, [] (auto&&, auto&& h) { update(h); });
        update(s.get());
    }
};
          

struct room_component
{
    Widget::Button button;

    template <typename T, typename Lens>
    room_component(state<T> s, Lens l, Widget::View& parent)
        : button{parent}
    {
        button.on_clicked.connect([s] {
            auto light_on = view(s.get(), l);
            s.set(put(s.get(), l, !light_on));
        });
        auto update = [=] (auto&& v) {
            button.SetText(view(v, l) ? "Turn off!" : "Turn on!");
        };
        watch(s, [] (auto&&, auto&& v) { update(v); });
        update(s.get());
    }
};

Naive lenses


template <typename Whole, typename Part>
using lens = std::pair<
    function<Part(Whole)>,         // getter
    function<Whole(Whole, Part)>,  // setter
>;

template <typename Whole, typename Lens>
auto view(Whole w, Lens l) {
    return l.first();
}

template <typename Whole, typename Lens, typename Part>
auto put(Whole w, Lens l, Part p) {
    return l.second(w, p);
}

auto room_light_on(room_id id) {
    return pair{
        [=] (house h) {
             return h.rooms[id].light_on;
        },
        [=] (house h, bool on) {
             h.rooms[id].light_on = on;
             return h;
        }
    };
}

auto st = state<house>{};
auto c0 = room_component{st, room_light_on(0), ...};
auto c1 = room_component{st, room_light_on(1), ...};
// ...

auto room_light_on(room_id id) {
    return attr(&house::rooms)
         | at_(id)
         | attr(&room::light_on);
}







auto st = state<house>{};
auto c0 = room_component{st, room_light_on(0), ...};
auto c1 = room_component{st, room_light_on(1), ...};
...

Van Laarhovens lenses


Lenses defined as one single function,
composable with $(f \circ g)(x) = f(g(x))$


Albert Steckermeier Lenses in Functional Programming
sinusoid.es/misc/lager/lenses.pdf
Bartosz Milewski Lenses, Stores, and Yoneda
bartoszmilewski.com/2013/10/08/lenses-stores-and-yoneda
godbolt.org/z/cyeawy
$😭$

struct room_component
{
    Widget::Button button;

    template <typename T, typename Lens>
    room_component(state<T> s, Lens l, Widget::View& parent)
        : button{parent}
    {
        button.on_clicked.connect([s] {
            auto light_on = view(s.get(), l);
            s.set(put(s.get(), l, !light_on));
        });
        auto update = [=] (auto&& v) {
            button.SetText(view(v, l) ? "Turn off!" : "Turn on!");
        };
        watch(s, [] (auto&&, auto&& v) { update(v); });
        update(s.get());
    }
};

template <typename T>
struct state
{
    const T& get();

    void set(T v);

    friend auto watch(state& s, std::function<const T&, const T&> cb);

    template <typename Lens>
    auto zoom(Lens l)
        -> cursor<decltype(view(get(), l))>;









};

template <typename T>
struct cursor
{
    const T& get();

    void set(T v);

    friend auto watch(state& s, std::function<const T&, const T&> cb);

    template <typename Lens>
    auto zoom(Lens l)
        -> cursor<decltype(view(get(), l))>;

private:
    struct node { // pseudo-code
       std::shared_ptr<node>> parent;
       std::vector<std::weak_ptr<node>> children;
       T curr;
       boost::signal<const T&, const T&> on_change;
    };
    std::shared_ptr<node> node_;
};
$😊$

struct room_component
{
    Widget::Button button;

    room_component(cursor<bool> s, Widget::View& parent)
        : button{parent}
    {
        button.on_clicked.connect([s] {
            s.set(!s.get());
        });
        auto update = [=] (bool on) {
            button.SetText(on ? "Turn off!" : "Turn on!");
        };
        watch(s, [] (bool, bool on) { update(on); });
        update(s.get());
    }
};

auto st = state<house>{};
auto c0 = room_component{
    st.zoom(attr(&house::rooms)
              | at_(0)
              | attr(&room::light_on)),
    ...
};

auto st = state<house>{};
auto c0 = room_component{
    st.zoom(attr(&house::rooms))
      .zoom(at_(0))
      .zoom(&room::light_on),
    ...
};

auto st = state<house>{};
auto c0 = room_component{
    st[&house::rooms][0][&room::light_on],
    ...
};



interfaces
  • reader<T>  ->   get, watch
  • writer<T>  ->   set
  • cursor<T>  ->   reader + writer

roots
  • state<T, automatic_tag>   ->   cursor
  • state<T, epochal_tag>     ->   cursor + commit
  • sensor<T>                 ->   reader + commit

transformations
  • .zoom(lens)                   ->   cursor [/ *]
  • .xf(xform[, xform2])          ->   reader [/ writer]
  • merge(...)(xform[, xform2])   ->   reader [/ writer]
  • .cross_thread()               ->   WIP
  cat   dedupe   distinct   drop   drop_while   filter   interpose   mapcat   map   map_indexed   partition_by   partition   random_sample   remove   replace   take   take_nth   take_while
     chain   count   cycle   enumerate   product   range   repeat

Exclusive!

  each   eager   interleave   iter   readbuf   read   sink   traced   transducer   unzip   writebuf   write   zip

github.com/arximboldi/zug

Transducers: from Clojure to C++

Juanpe Bolívar, CppCon '15

youtu.be/vohGJjGxtJQ

Part IV

Example

model.cpp


class Item : public QObject
{
    Q_OBJECT
    bool done_ = 0;
    ...
public:
    Q_PROPERTY(bool   done
               READ   getDone
               WRITE  setDone
               NOTIFY doneChanged)

    int getDone() const {
        return done_;
    }
    void setDone(bool v) {
        done_ = v;
        Q_EMIT doneChanged();
    }
    Q_SIGNAL doneChanged();
};
            

gui.qml


Window {
    id: root

    Todo.Item {
      id: model
    }

    Text {
       text: model.text
       color: model.done ? "grey" : "black"
    }
    Button {
       text: model.done ? "Mark" : "Unmark"
       onClick: model.done = !model.done
    }
}





            

namespace todo {

struct item
{
    bool done = false;
    std::string text;
};

struct model
{
    std::string name;
    immer::flex_vector<item> todos;
};

model add_item(model m, std::string text);
model remove_item(model m, std::size_t index);
model load(const std::string& fname);
model save(const std::string& fname, const model& todos);

} // namespace todo
          

class Item : public QObject {
    Q_OBJECT
    lager::cursor<bool> done_;
    lager::cursor<QString> text_;
public:
    Q_PROPERTY(bool done READ getDone WRITE setDone NOTIFY doneChanged);
    bool getDone() const { return done_.get(); }
    void setDone(bool done) { done_.set(done); }
    Q_SIGNAL void doneChanged();

    Q_PROPERTY(QString text READ getText WRITE setText NOTIFY textChanged);
    QString getText() const { return text_.get(); }
    void setText(QString text) { text_.set(text); }
    Q_SIGNAL void textChanged();

    Item(lager::cursor<todo::item> data)
        : done_{data[&todo::item::done]}
        , text_{data[&todo::item::text].xf(
             zug::map([](auto x) { return QString::fromStdString(x); }),
             zug::map([](auto x) { return x.toStdString(); }))}
    {
        watch(done_, [&](auto&&...) { Q_EMIT doneChanged(); });
        watch(text_, [&](auto&&...) { Q_EMIT textChanged(); });
    }
};
          

class Item : public QObject
{
    Q_OBJECT

public:
    LAGER_QT_CURSOR(bool, done);
    LAGER_QT_CURSOR(QString, text);

    Item(lager::cursor<todo::item> data)
        : LAGER_QT(done) {data[&todo::item::done]}
        , LAGER_QT(text) {data[&todo::item::text].xf(
            zug::map([](auto&& x) { return QString::fromStdString(x); }),
            zug::map([](auto&& x) { return x.toStdString(); }))}
    {}
};
          

class Model : public QObject
{
    Q_OBJECT 
    lager::state<todo::model, lager::automatic_tag> state_;
    
public: 
    LAGER_QT_CURSOR(QString, name);
    LAGER_QT_READER(int, count);
    
    Model()
        : LAGER_QT(name){state_[&todo::model::name].xf(
              zug::map([](auto&& x) { return QString::fromStdString(x); }),
              zug::map([](auto&& x) { return x.toStdString(); }))}
        , LAGER_QT(count){state_.xf(zug::map(
              [](auto&& x) { return (int) x.todos.size(); }))}
    {}

    Q_INVOKABLE Item* todo(int index)
    {
        return new Item{state_[&todo::model::todos][index]};
    }

    ...
};
          

class Model : public QObject
{
    ...

    Q_INVOKABLE void add(QString text)
    {
        state_.update([&](auto m) {
            return todo::add_item(
                x, text.toStdString());
        });
    }

    Q_INVOKABLE void remove(int idx)
    {
        state_.update([&](auto m) {
            return todo::remove_item(x, idx);
        });
    }

    ...
            

              


    ...

    Q_INVOKABLE void save(QString fname)
    {
        state_.update([&](auto x) {
            return todo::save(
                fname.toStdString(), x);
        });
    }

    Q_INVOKABLE void load(QString fname)
    {
        state_.update([&](auto x) {
            return todo::load(
                fname.toStdString());
        });
    }

};
            

struct item
{
    bool done = false;
    std::string text;
};

struct model
{
    std::string name;
    immer::flex_vector<item> todos;
};
          

class Item : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool done ...);
    Q_PROPERTY(QString text ...);
};

class Model : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name ...);
    Q_PROPERTY(int count ...);
    Q_INVOKABLE Item* item(int idx);
    ...
};
            

Conclusion

Cursors


  • Resolve the impedance mismatch
    value-based data-model ⬌ object-oriented UI
  • Incremental redesign an
    object-oriented data-model ➡ value-oriented


    Carl Bussey   github.com/cjbussey
María Carrasco   github.com/kostspielig

Objects vs Values: Value Oriented Programming in an Object Oriented World

Tony Van Eerd, CppCon'19

youtu.be/2JGH_SWURrI
“Utopia is on the horizon. I move two steps closer; it moves two steps away. I walk another ten steps and the horizon runs ten steps further away. As much as I may walk, I'll never reach it.
So what's the point of utopia?
That's the point—
to keep walking.”

Eduardo Galeano
“Utopia is on the horizon. I move two steps closer; it moves two steps away. I walk another ten steps and the horizon runs ten steps further away. As much as I may walk, I'll never reach it.
So what's the point of utopia?
That's the point—
to keep walking.”

Eduardo Galeano

EXTRAS

Value composition

Van Laarhoven Lens


template <typename Whole, typename Part>
using lens = std::pair<
    function<Part(Whole)>,
    function<Whole(Whole, Part)>,
>;



template <typename Whole, typename Part>
auto attr(Part Whole::* member)
{
    return pair{
        [=] (auto w) {
             return w.*member;
        }, [=] (auto w, auto p) {
             w.*memeber = p;
             return w;
        }
    };
}
          

Requires custom composition... $😞$


template <typename Whole, typename Part>
using lens = function<
    function<Functor<Whole>(Whole)> (
        function<Functor<Part>(Part)>)
>;
// pseudo-C++ for
// Functor f => (a -> f a) -> (s -> f s)

template <typename Whole, typename Part>
auto attr(Part Whole::* member)
{
    return [=](auto f) {
        return [=](auto w) {
            return f(w.*member)([&](auto p) {
                w.*member = p;
                return w;
            });
        };
    };
}
          

Composable with $(f \circ g)(x) = f(g(x))$

Modularity Redux


struct room_model
{
    string name;
    bool light_on;
};

struct toggle_light_action {};
struct ...;

using room_action = variant<
    toggle_light_action,
    ...
>;







          


room_model update(room_model m,
                  room_action a)
{
    return visit(lager::visitor{
        [&] (toggle_light_action&&) {
            m.light_on = !m.light_on;
            return m;
        },
        [&] (...) { ... }
    }, std::move(a));
}

void draw(const room_model& m,
          lager::context<room_action> ctx)
{
    if (ImGui::Button(m.light_on ? "Turn off" :
                                   "Turn on")) {
        ctx.dispatch(toggle_light_action{});
    }
}
          

struct house_model
{
    vector<room_model> rooms;
    vector<pair<int, int>> doors;
};

using house_action = variant<
    pair<int, room_action>,
    ...
>;














          

house_model update(house_model m,
                   house_action a)
{
    return visit(lager::visitor{
        [&] (pair<int, room_action>&& a) {
            auto& r = m.rooms[a.first];
            r = update(r, a.second);
            return m;
        },
        [&] (...) { ... }
    }, std::move(a));
}

void draw(const house_model& m,
          lager::context<house_action> ctx)
{
    auto idx = std::size_t;
    for (auto&& r : m.rooms) {
       draw(r, {ctx, [i = idx++] (auto a) {
          return make_pair(i, a);
       }});
    }
}
          

struct model { ... };
using action = ...;

model update(model m, action a);
void draw(const model& m, lager::context<action> ctx);

int main()
{
    auto store = lager::make_store<action>(
            model{},
            update,
            lager::with_manual_event_loop{});

    watch(store, [&](auto&& old, auto&& cur) {
        draw(cur, store);        // redraw on change
    });

    while (auto ev = poll()) {   // poll external input
        auto act = intent(ev);   // map event to action
        store.dispatch(act);
    }
}

[ Disclaimer ]