Page MenuHomePhorge

No OneTemporary

Size
21 KB
Referenced Files
None
Subscribers
None
diff --git a/CMakeLists.txt b/CMakeLists.txt
index dc966a5..319b947 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,34 +1,35 @@
cmake_minimum_required(VERSION 3.16)
project(lager-qt-demo)
include(FeatureSummary)
set(QT_MAJOR_VERSION 6)
set(KF_MAJOR_VERSION 6)
set(QT_MIN_VERSION 6.5.0)
set(KF_MIN_VERSION 6.0.0)
find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH})
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
include(ECMSetupVersion)
include(ECMGenerateHeaders)
include(ECMPoQmTools)
include(ECMAddTests)
include(ECMQtDeclareLoggingCategory)
include(ECMDeprecationSettings)
include(ECMQmlModule)
kde_enable_exceptions()
find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS Core Gui Qml QuickControls2 Widgets Test)
qt6_policy(SET QTP0001 NEW)
find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami)
find_package(Threads REQUIRED)
find_package(Lager REQUIRED)
+find_package(libkazv)
add_subdirectory(src)
diff --git a/doc.org b/doc.org
index 41c9cb8..fd1ba6e 100644
--- a/doc.org
+++ b/doc.org
@@ -1,125 +1,126 @@
#+PROPERTY: header-args :exports code
* What is lager?
- library to help with *value-oriented design*
- store application state as a *value* type
- state transformations are described in pure functions (*reducers*), src_c++{auto update(Model, Action) -> Model;}
- actions are also value types
* What is a value type?
- copyable
- equality comparable
#+BEGIN_SRC c++
T a = ...;
T b = a;
REQUIRE(a == b);
#+END_SRC
- identity does not depend on a reference
#+BEGIN_SRC c++
// not a value type
struct Person
{
std::string name;
std::vector<std::weak_ptr<Person>> contacts;
};
// value type
struct Person
{
using IdType = std::string;
IdType id;
std::string name;
std::vector<IdType> contacts;
};
#+END_SRC
* Basic example of a value-oriented program
#+INCLUDE: "src/basic-example/logic.hpp" src c++
#+INCLUDE: "src/basic-example/logic.cpp" src c++
#+INCLUDE: "src/basic-example/main.cpp" src c++
The basic example just covered doesn't even make use of lager (!!!)
* Benefits of using lager?
- Testability
- libkazv: [[https://lily-is.land/kazv/libkazv][76.9%]] test coverage
- libQuotient: [[https://sonarcloud.io/component_measures?metric=coverage&selected=quotient-im_libQuotient%3AQuotient&id=quotient-im_libQuotient][41.6%]] (raw), 46.9% (adjusted)
#+INCLUDE: "src/basic-example/test.cpp" src c++
- Offload work into worker threads
#+BEGIN_SRC c++
QtConcurrent::run([model]() { // capture model by value
save(model);
});
#+END_SRC
- Performance of copying? Structural sharing, immer containers
- Undo/redo for free
- History is a list of models
- Undo/redo = changing the current index
- Normal actions = push a new model onto the list
* How to do IO?
** Reducers revisited
- src_c++{auto update(Model, Action) -> Model;}
- But also src_c++{auto update(Model, Action) -> std::pair<Model, Effect>;}
- What is an effect? A (maybe non-pure) function that takes a *context* as an argument.
- A *store* contains the model and the context. It also has an event loop. Reducers will be run in the event loop.
- The store can be constructed with *dependencies*, which can be accessed through the context.
- The context can also be used to dispatch actions.
#+INCLUDE: "src/effects-example/main.cpp" src c++
* How can lager help you integrate value-oriented design with a Qt application?
** Missing part: view
- In the example, src_c++{show()} is the view.
- We want to show the state as a src_c++{QWidget} or src_c++{QQuickItem}.
- Signal/slot connections to update display values.
- [[https://sinusoid.es/lager/cursors.html][Cursors]]
- src_c++{lager::reader} - readonly cursors
- readers can be watched
- src_c++{lager::store} is a reader
- map-transformations and some lenses
- src_c++{LAGER_QT} and src_c++{LAGER_QT_READER}
* Practical application problems
* List views
[[https://github.com/arximboldi/lager/issues/179]]
** Number as model in QML ListView
=main.cpp=
#+INCLUDE: "src/list-view/main.cpp" src c++
=demo-app.hpp=
#+INCLUDE: "src/list-view/demo-app.hpp" src c++
=demo-app.cpp=
#+INCLUDE: "src/list-view/demo-app.cpp" src c++
=demo-list.hpp=
#+INCLUDE: "src/list-view/demo-list.hpp" src c++
=demo-list.cpp=
#+INCLUDE: "src/list-view/demo-list.cpp" src c++
=demo-item.hpp=
#+INCLUDE: "src/list-view/demo-item.hpp" src c++
=demo-item.cpp=
#+INCLUDE: "src/list-view/demo-item.cpp" src c++
=Main.qml=
#+INCLUDE: "src/list-view/Main.qml" src qml
** Using a src_c++{QAbstractListModel}
* Post-dispatch then-continuations
[[https://github.com/arximboldi/lager/issues/96]], [[https://github.com/arximboldi/lager/issues/106]]
- lager does not support this
- Resolved by creating another kind of store
- Effects return a Promise (for C++ programmers, it's more like src_c++{std::future}) instead of void
- src_c++{dispatch} can be src_c++{then}-ed, the callback will be run after the returned future becomes ready
+#+INCLUDE: "src/libkazvstore-example/main.cpp" src c++
* Offloading work into worker threads
- src_c++{ctx.dispatch} is thread-safe, can use it to pass down the result of a heavy computation
- When heavy computation needs to be atomic, put the event loop into a worker thread
- Create a secondary store that has an event loop in the UI thread
- This is needed because cursor operations are not thread-safe [[https://github.com/arximboldi/lager/issues/118]]
* Using src_c++{lager::state} (leave to Dmitry)
* Appendix
** Calculating libQuotient coverage adjusted
libQuotient includes the generated csapi folder into coverage, while libkazv does not. Here is the code that calculates the coverage of libQuotient excluding the csapi folder.
#+NAME: libquotient-coverage
#+BEGIN_SRC perl
my $csapiCoverage = 0.169;
my $csapiUncovered = 1_668;
my $totalLines = 11_229;
my $totalUncovered = 6_561;
my $csapiLines = $csapiUncovered / (1-$csapiCoverage);
my $remainingLines = $totalLines - $csapiLines;
my $remainingUncovered = $totalUncovered - $csapiUncovered;
my $remainingCoverage = 1 - $remainingUncovered / $remainingLines;
$remainingCoverage;
#+END_SRC
#+RESULTS: libquotient-coverage
: 0.46940827964562
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0ff893e..0e900c4 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,3 +1,6 @@
add_subdirectory(basic-example)
add_subdirectory(effects-example)
add_subdirectory(list-view)
+if(libkazv_FOUND)
+ add_subdirectory(libkazvstore-example)
+endif()
diff --git a/src/effects-example/main.cpp b/src/effects-example/main.cpp
index a7f4aea..a55485b 100644
--- a/src/effects-example/main.cpp
+++ b/src/effects-example/main.cpp
@@ -1,92 +1,96 @@
#include <iostream>
#include <variant>
#include <boost/asio.hpp>
#include <lager/event_loop/qt.hpp>
#include <lager/store.hpp>
#include <QObject>
#include <QCoreApplication>
#include <QTimer>
struct Model
{
int value{0};
};
struct IncrementAction {};
struct IncrementTwiceAction {};
using Action = std::variant<IncrementAction, IncrementTwiceAction>;
struct IOHandler
{
template<class T>
void show(T x)
{
std::cout << "-->" << x << std::endl;
}
};
using DepsT = lager::deps<IOHandler &>;
std::pair<Model, lager::effect<Action, DepsT>> update(Model m, IncrementAction)
{
- return {Model{m.value + 1}, [m](const auto &ctx) {
+ m.value += 1;
+ return {m, [m](const auto &ctx) {
auto &h = ctx.template get<IOHandler &>();
h.show(m.value);
h.show("increment action");
}};
}
std::pair<Model, lager::effect<Action, DepsT>> update(Model m, IncrementTwiceAction)
{
return {m, [](const auto &ctx) {
ctx.dispatch(IncrementAction{});
ctx.dispatch(IncrementAction{});
}};
}
std::pair<Model, lager::effect<Action, DepsT>> update(Model m, Action a)
{
return std::visit([&](const auto &action) {
return update(m, action);
}, a);
}
void repl(QObject *obj, lager::context<Action> ctx)
{
std::cout << "?" << std::endl;
std::string a;
std::cin >> a;
if (a == "1") {
ctx.dispatch(IncrementAction{});
} else if (a == "2") {
ctx.dispatch(IncrementTwiceAction{});
} else {
std::cout << "unrecognized action" << std::endl;
}
+ // XXX Why can't I just use QMetaObject::invokeMethod
+ // with QueuedConnection?
+ // Let's bear with it now. Will go back to this later.
QTimer::singleShot(100, obj, [=]() {
repl(obj, ctx);
});
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QObject mainObject;
IOHandler handler;
auto store = lager::make_store<Action>(
Model{},
lager::with_qt_event_loop{mainObject},
lager::with_deps(std::ref(handler)));
auto context = static_cast<lager::context<Action>>(store);
QTimer::singleShot(0, &mainObject,
[obj=&mainObject, context]() {
repl(obj, context);
});
return app.exec();
}
diff --git a/src/libkazvstore-example/CMakeLists.txt b/src/libkazvstore-example/CMakeLists.txt
new file mode 100644
index 0000000..ba12aeb
--- /dev/null
+++ b/src/libkazvstore-example/CMakeLists.txt
@@ -0,0 +1,8 @@
+
+add_executable(libkazvstore-example main.cpp qt-promise-handler.cpp)
+target_link_libraries(libkazvstore-example PRIVATE
+ Qt6::Core
+ Qt6::Concurrent
+ lager
+ libkazv::kazvstore
+)
diff --git a/src/libkazvstore-example/kazv-defs.hpp b/src/libkazvstore-example/kazv-defs.hpp
new file mode 100644
index 0000000..5cfa98f
--- /dev/null
+++ b/src/libkazvstore-example/kazv-defs.hpp
@@ -0,0 +1,4 @@
+
+#pragma once
+#include <immer/config.hpp>
+#include <lager/config.hpp>
diff --git a/src/effects-example/main.cpp b/src/libkazvstore-example/main.cpp
similarity index 57%
copy from src/effects-example/main.cpp
copy to src/libkazvstore-example/main.cpp
index a7f4aea..8a8d327 100644
--- a/src/effects-example/main.cpp
+++ b/src/libkazvstore-example/main.cpp
@@ -1,92 +1,100 @@
#include <iostream>
#include <variant>
#include <boost/asio.hpp>
#include <lager/event_loop/qt.hpp>
-#include <lager/store.hpp>
+#include <store.hpp>
#include <QObject>
#include <QCoreApplication>
#include <QTimer>
+#include "qt-promise-handler.hpp"
struct Model
{
int value{0};
};
struct IncrementAction {};
struct IncrementTwiceAction {};
using Action = std::variant<IncrementAction, IncrementTwiceAction>;
struct IOHandler
{
template<class T>
void show(T x)
{
std::cout << "-->" << x << std::endl;
}
};
using DepsT = lager::deps<IOHandler &>;
-std::pair<Model, lager::effect<Action, DepsT>> update(Model m, IncrementAction)
+std::pair<Model, Kazv::Effect<Action, DepsT>> update(Model m, IncrementAction)
{
- return {Model{m.value + 1}, [m](const auto &ctx) {
+ m.value += 1;
+ return {m, [m](const auto &ctx) {
auto &h = ctx.template get<IOHandler &>();
h.show(m.value);
h.show("increment action");
}};
}
-std::pair<Model, lager::effect<Action, DepsT>> update(Model m, IncrementTwiceAction)
+std::pair<Model, Kazv::Effect<Action, DepsT>> update(Model m, IncrementTwiceAction)
{
return {m, [](const auto &ctx) {
- ctx.dispatch(IncrementAction{});
- ctx.dispatch(IncrementAction{});
+ return ctx.dispatch(IncrementAction{})
+ .then([ctx]([[maybe_unused]] auto status) {
+ return ctx.dispatch(IncrementAction{});
+ });
}};
}
-std::pair<Model, lager::effect<Action, DepsT>> update(Model m, Action a)
+std::pair<Model, Kazv::Effect<Action, DepsT>> update(Model m, Action a)
{
return std::visit([&](const auto &action) {
return update(m, action);
}, a);
}
-void repl(QObject *obj, lager::context<Action> ctx)
+void repl(QObject *obj, Kazv::Context<Action> ctx)
{
std::cout << "?" << std::endl;
std::string a;
std::cin >> a;
+ auto promise = ctx.createResolvedPromise({});
if (a == "1") {
- ctx.dispatch(IncrementAction{});
+ promise = ctx.dispatch(IncrementAction{});
} else if (a == "2") {
- ctx.dispatch(IncrementTwiceAction{});
+ promise = ctx.dispatch(IncrementTwiceAction{});
} else {
std::cout << "unrecognized action" << std::endl;
}
- QTimer::singleShot(100, obj, [=]() {
+ promise.then([obj, ctx]([[maybe_unused]] auto status) {
repl(obj, ctx);
});
}
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QObject mainObject;
IOHandler handler;
- auto store = lager::make_store<Action>(
+ auto store = Kazv::makeStore<Action>(
Model{},
- lager::with_qt_event_loop{mainObject},
+ [](auto m, auto a) {
+ return update(m, a);
+ },
+ QtPromiseHandler{mainObject},
lager::with_deps(std::ref(handler)));
- auto context = static_cast<lager::context<Action>>(store);
+ auto context = store.context();
QTimer::singleShot(0, &mainObject,
[obj=&mainObject, context]() {
repl(obj, context);
});
return app.exec();
}
diff --git a/src/libkazvstore-example/qt-promise-handler.cpp b/src/libkazvstore-example/qt-promise-handler.cpp
new file mode 100644
index 0000000..15e1aad
--- /dev/null
+++ b/src/libkazvstore-example/qt-promise-handler.cpp
@@ -0,0 +1,15 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#include <kazv-defs.hpp>
+
+#include "qt-promise-handler.hpp"
+
+QtPromiseSignalTrigger::QtPromiseSignalTrigger(QObject *parent)
+ : QObject(parent)
+{}
+
+QtPromiseSignalTrigger::~QtPromiseSignalTrigger() = default;
diff --git a/src/libkazvstore-example/qt-promise-handler.hpp b/src/libkazvstore-example/qt-promise-handler.hpp
new file mode 100644
index 0000000..93d63c1
--- /dev/null
+++ b/src/libkazvstore-example/qt-promise-handler.hpp
@@ -0,0 +1,253 @@
+/*
+ * This file is part of kazv.
+ * SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+#pragma once
+#include <kazv-defs.hpp>
+
+#include <future>
+
+#include <QObject>
+#include <QMetaObject>
+#include <QPointer>
+#include <QTimer>
+
+#include <promise-interface.hpp>
+
+template<class T>
+class QtPromise;
+
+namespace QtPromiseDetail
+{
+ struct QtPromiseHelper
+ {
+ template<class T>
+ using PromiseType = QtPromise<T>;
+ };
+
+ struct IdentityFunc
+ {
+ template<class T>
+ constexpr T &&operator()(T &&t) const {
+ return std::forward<T>(t);
+ }
+ };
+
+ template<class Func>
+ void post(QObject *obj, Func &&func)
+ {
+ QMetaObject::invokeMethod(obj, std::forward<Func>(func), Qt::QueuedConnection);
+ }
+}
+
+class QtPromiseSignalTrigger : public QObject
+{
+ Q_OBJECT
+public:
+ QtPromiseSignalTrigger(QObject *parent = 0);
+
+ ~QtPromiseSignalTrigger();
+
+Q_SIGNALS:
+ void finished();
+};
+
+template<class T>
+class QtPromise : public Kazv::AbstractPromise<QtPromiseDetail::QtPromiseHelper::PromiseType, T>
+{
+ using BaseT = Kazv::AbstractPromise<QtPromiseDetail::QtPromiseHelper::PromiseType, T>;
+
+ template<class FuncT, class PromiseT, class ResolveT>
+ struct WaitHelper
+ {
+ void resolveOrWait()
+ {
+ auto res = func(p.get());
+ using ResT = decltype(res);
+ if constexpr (Kazv::isPromise<ResT>) {
+ res.then([resolve=resolve](auto val) {
+ resolve(std::move(val));
+ return std::decay_t<decltype(val)>(); // we don't care about this value
+ });
+ } else {
+ resolve(res);
+ }
+ }
+
+ void wait()
+ {
+ if (p.ready()) {
+ QtPromiseDetail::post(
+ executor,
+ [*this]() mutable { resolveOrWait(); }
+ );
+ } else {
+ QObject::connect(
+ p.m_signalTrigger, &QtPromiseSignalTrigger::finished,
+ executor, [*this]() mutable {
+ resolveOrWait();
+ }
+ );
+ }
+ }
+
+ QPointer<QObject> executor;
+ PromiseT p;
+ FuncT func;
+ ResolveT resolve;
+ };
+
+ struct ResolveHelper
+ {
+ template<class ValT>
+ void operator()(ValT val) const
+ {
+ using ResT = std::decay_t<decltype(val)>;
+ if constexpr (Kazv::isPromise<ResT>) {
+ QtPromiseDetail::post(
+ executor,
+ [qtPromise=std::move(val), *this]() mutable {
+ if (qtPromise.ready()) {
+ setValue(qtPromise.get());
+ } else {
+ qtPromise.then([*this](auto val) mutable {
+ setValue(std::move(val));
+ return std::decay_t<decltype(val)>(); // we don't care about this value
+ });
+ }
+ });
+ } else {
+ QtPromiseDetail::post(
+ executor,
+ [*this, val=std::move(val)]() mutable {
+ setValue(std::move(val));
+ });
+ }
+ }
+
+ void setValue(T resolvedValue) const
+ {
+ p->set_value(std::move(resolvedValue));
+ if (signalTrigger) {
+ Q_EMIT signalTrigger->finished();
+ signalTrigger->deleteLater();
+ }
+ }
+
+ QPointer<QObject> executor;
+ QPointer<QtPromiseSignalTrigger> signalTrigger;
+ std::shared_ptr<std::promise<T>> p;
+ };
+public:
+ QtPromise(QObject *executor, T value)
+ : BaseT(this)
+ , m_executor(executor)
+ , m_signalTrigger()
+ {
+ std::promise<T> p;
+ m_val = p.get_future().share();
+ p.set_value(std::move(value));
+ }
+
+ template<class Func>
+ QtPromise(QObject *executor, Func &&callback)
+ : BaseT(this)
+ , m_executor(executor)
+ , m_signalTrigger(new QtPromiseSignalTrigger())
+ {
+ auto p = std::make_shared<std::promise<T>>();
+ m_val = p->get_future().share();
+
+ m_signalTrigger->moveToThread(m_executor->thread());
+ auto resolve = ResolveHelper{m_executor, m_signalTrigger, p};
+
+ QtPromiseDetail::post(
+ m_executor,
+ [=, callback=std::forward<Func>(callback),
+ resolve=std::move(resolve)]() {
+ callback(resolve);
+ });
+ }
+
+ // FuncT: (DataT) -> AnotherDataT
+ // where AnotherDataT = PromiseThenResult<FuncT, typename BaseT::DataT>
+ template<class FuncT>
+ auto then(FuncT &&func)
+ -> QtPromise<Kazv::PromiseThenResult<FuncT, typename BaseT::DataT>> {
+ return QtPromise<Kazv::PromiseThenResult<FuncT, typename BaseT::DataT>>(
+ m_executor,
+ [=, func=std::forward<FuncT>(func), *this](auto resolve) {
+ auto waitHelper = WaitHelper<std::decay_t<FuncT>,
+ QtPromise,
+ std::decay_t<decltype(resolve)>>{
+ m_executor,
+ *this,
+ func,
+ resolve
+ };
+ waitHelper.wait();
+ });
+ }
+
+ bool ready() const {
+ return m_val.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
+ }
+
+ T get() const {
+ return m_val.get();
+ }
+private:
+ QPointer<QObject> m_executor;
+ QPointer<QtPromiseSignalTrigger> m_signalTrigger;
+ std::shared_future<T> m_val;
+};
+
+class QtPromiseHandler : public Kazv::PromiseInterface<QtPromiseHandler,
+ QtPromiseDetail::QtPromiseHelper::PromiseType>
+{
+ using BaseT = Kazv::PromiseInterface<QtPromiseHandler,
+ QtPromiseDetail::QtPromiseHelper::PromiseType>;
+public:
+ template<class T>
+ using PromiseT = QtPromise<T>;
+
+ QtPromiseHandler(std::reference_wrapper<QObject> executor)
+ : BaseT(this)
+ , m_executor(&executor.get())
+ {}
+
+ QtPromiseHandler(const QtPromiseHandler &that)
+ : BaseT(this)
+ , m_executor(that.m_executor)
+ {}
+
+ QtPromiseHandler(QtPromiseHandler &&that)
+ : BaseT(this)
+ , m_executor(std::move(that.m_executor))
+ {}
+
+ QtPromiseHandler &operator=(const QtPromiseHandler &that) {
+ m_executor = that.m_executor;
+ return *this;
+ }
+
+ QtPromiseHandler &operator=(QtPromiseHandler &&that) {
+ m_executor = std::move(that.m_executor);
+ return *this;
+ }
+
+ template<class T, class FuncT>
+ PromiseT<T> create(FuncT &&func) {
+ return PromiseT<T>(m_executor, std::forward<FuncT>(func));
+ }
+
+ template<class T>
+ PromiseT<T> createResolved(T val) {
+ return PromiseT<T>(m_executor, std::move(val));
+ }
+
+private:
+ QPointer<QObject> m_executor;
+};

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 24, 4:48 AM (18 h, 47 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39347
Default Alt Text
(21 KB)

Event Timeline