morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
signal.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <concepts>
5#include <cstddef>
6#include <cstdint>
7#include <exception>
8#include <functional>
9#include <memory>
10#include <optional>
11#include <stdexcept>
12#include <type_traits>
13#include <utility>
14
15#include "detail/graph.hpp"
16#include "runtime.hpp"
17
18/// @file
19/// @brief `Signal<T>`, `Computed<T>` and `Effect`: the nodes of a reactive graph.
20///
21/// Specified in `docs/spec/reactive/signals.md`.
22
23namespace morph::reactive {
24
25/// @brief Whether a `Signal` or `Computed` compares a new value with the current one before notifying.
26enum class EqualityPolicy : std::uint8_t {
27 /// Skip a value equal to the current one, when `==` is usable for the type (`kEqualityUsable`).
29 /// Notify on every write or recomputation, even of an equal value.
31};
32
33/// @brief Whether `==` is usable for @p T, which is what the `EqualityPolicy::Auto` skip needs.
34///
35/// Checked through element types, not by `std::equality_comparable` alone: a standard container declares `==`
36/// for any element type, and comparing two of them fails to compile when the element has none. A type for which
37/// this is false always notifies.
38/// @tparam T The value type.
39template <typename T>
41
42/// @brief A source value. Reading it with `get()` inside a tracked run subscribes that run.
43///
44/// `set()` skips a value equal to the current one when `==` is usable for `T` (`kEqualityUsable`) and the
45/// policy is `EqualityPolicy::Auto`; `mutate()` always notifies. Writes are refused, and reported, off the
46/// owner or inside a Computed. A read off the owner is reported and subscribes nothing.
47/// @tparam T The value type; move-constructible and move-assignable.
48template <typename T>
49class Signal final : public detail::Node {
50public:
51 /// @brief Constructs a value-initialised signal.
52 /// @param runtime The runtime this signal belongs to.
53 explicit Signal(Runtime& runtime)
54 requires std::default_initializable<T>
55 : Node{runtime.core()}, _value{} {}
56
57 /// @brief Constructs a signal holding @p initial.
58 /// @param runtime The runtime this signal belongs to.
59 /// @param initial The initial value.
60 /// @param policy Whether a write equal to the current value is skipped.
61 Signal(Runtime& runtime, T initial, EqualityPolicy policy = EqualityPolicy::Auto)
62 : Node{runtime.core()}, _value{std::move(initial)}, _policy{policy} {}
63
64 ~Signal() override = default;
65 Signal(Signal const&) = delete;
66 Signal& operator=(Signal const&) = delete;
67 Signal(Signal&&) = delete;
68 Signal& operator=(Signal&&) = delete;
69
70 /// @brief Reads the value, subscribing the innermost tracked run.
71 ///
72 /// Off the owner the read is reported (`detail::site::kOffOwner`) and proceeds untracked: it
73 /// subscribes nothing and leaves the tracking state alone. The value it returns is then read
74 /// without synchronisation against the owner's writes.
75 /// @return The current value; valid until the next write.
76 [[nodiscard]] T const& get() const {
77 if (core().checkOwner()) {
78 trackRead();
79 }
80 return _value;
81 }
82
83 /// @brief Reads the value without subscribing anything.
84 /// @return The current value; valid until the next write.
85 [[nodiscard]] T const& peek() const noexcept { return _value; }
86
87 /// @brief Replaces the value and notifies observers, unless it equals the current one and the policy is
88 /// `EqualityPolicy::Auto`.
89 /// @param value The new value.
90 void set(T value) {
91 if (!writable()) {
92 return;
93 }
94 if constexpr (kEqualityUsable<T>) {
95 if (_policy == EqualityPolicy::Auto && _value == value) {
96 return;
97 }
98 }
99 _value = std::move(value);
100 notify();
101 }
102
103 /// @brief Changes the value in place and always notifies observers.
104 /// @tparam F A callable taking `T&`.
105 /// @param fn The mutation.
106 template <typename F>
107 requires std::invocable<F, T&>
108 void mutate(F&& fn) {
109 if (!writable()) {
110 return;
111 }
112 std::invoke(std::forward<F>(fn), _value);
113 notify();
114 }
115
116private:
117 [[nodiscard]] bool writable() const noexcept {
118 if (!core().checkOwner()) {
119 return false;
120 }
121 if (core().isComputing()) {
123 return false;
124 }
125 return true;
126 }
127
128 void notify() {
129 detail::BatchScope const batch{core()};
130 markObserversStale(detail::Colour::Dirty);
131 }
132
133 // A source has nothing to recompute: a write marks its observers itself.
134 bool recompute() override { return false; } // NOLINT(portability-template-virtual-member-function)
135
136 T _value;
137 EqualityPolicy _policy = EqualityPolicy::Auto;
138};
139
140/// @brief A derived value: computed lazily on first read, cached, and recomputed only after a source it
141/// read really changed.
142///
143/// Observers are notified when the result changes. When `==` is usable for `T` (`kEqualityUsable`) and the
144/// policy is `EqualityPolicy::Auto`, an equal result stops propagation; otherwise every recomputation
145/// propagates.
146///
147/// A computation that throws leaves the Computed failed, and that is a change: observers re-run. While it
148/// is failed, a read behaves as follows:
149/// - the first read after the failing run rethrows that run's exception without computing again;
150/// - every later read retries the computation, returning its value if it now succeeds and rethrowing
151/// if it fails again.
152///
153/// A retry that fails again counts as unchanged and notifies nobody, so readers that retry do not wake
154/// each other. A failure after a source changed always propagates, as does a recovery, even to the value
155/// the Computed had before it failed, because its observers last saw a failure. A retry that recovers
156/// outside a flush posts one, so the observers it wakes run. A failed run keeps every source it has read,
157/// before and during that run, so a later change to any of them recomputes it.
158///
159/// Writing a signal inside the computation is reported and refused. So is reading the Computed while it
160/// is being brought up to date: from inside its own computation, or from the computation of a node it
161/// reads (a cycle, which may form only after the first evaluation). Such a read throws
162/// `std::logic_error`.
163///
164/// A computation may destroy its own Computed: the callable stays alive until it returns, and the
165/// Computed is not touched afterwards.
166/// @tparam T The value type; move-constructible. It need not be default-constructible.
167template <typename T>
168class Computed final : public detail::Node {
169public:
170 /// @brief Constructs the Computed without computing it: the first read does.
171 /// @tparam F A callable taking no arguments and returning something convertible to `T`; it may be
172 /// move-only.
173 /// @param runtime The runtime this computed belongs to.
174 /// @param fn The computation; every signal or computed it reads becomes a dependency.
175 /// @param policy Whether a result equal to the last one stops propagation.
176 template <typename F>
177 requires std::invocable<F&> && std::move_constructible<F> && std::convertible_to<std::invoke_result_t<F&>, T>
179 : Node{runtime.core()}, _fn{std::make_shared<F>(std::move(fn))}, _compute{&compute<F>}, _policy{policy} {
180 forceDirty();
181 }
182
183 ~Computed() override = default;
184 Computed(Computed const&) = delete;
185 Computed& operator=(Computed const&) = delete;
186 Computed(Computed&&) = delete;
187 Computed& operator=(Computed&&) = delete;
188
189 /// @brief Reads the value, bringing it up to date first, and subscribes the innermost tracked run.
190 ///
191 /// Off the owner the read is reported (`detail::site::kOffOwner`) and refused: it recomputes
192 /// nothing and subscribes nothing, and returns the value the last successful computation produced,
193 /// read without synchronisation against the owner.
194 /// @return The current value; valid until the next recomputation.
195 /// @throws Whatever the computation throws. `std::logic_error` when the Computed is read while it is
196 /// being brought up to date (reported, `detail::site::kComputedReadsItself`), when its
197 /// computation destroys it, or off the owner when it has never been computed successfully.
198 [[nodiscard]] T const& get() const {
199 if (!core().checkOwner()) {
200 return cached();
201 }
202 refuseSelfRead();
203 trackRead();
204 return mutableSelf().current();
205 }
206
207 /// @brief Reads the value, bringing it up to date first, without subscribing anything.
208 ///
209 /// Off the owner it behaves as `get()` does there.
210 /// @return The current value; valid until the next recomputation.
211 /// @throws Whatever `get()` throws.
212 [[nodiscard]] T const& peek() const {
213 if (!core().checkOwner()) {
214 return cached();
215 }
216 refuseSelfRead();
217 detail::TrackingFrame const untracked{core(), nullptr};
218 return mutableSelf().current();
219 }
220
221private:
222 // Tells the runtime a computation is running, so a write inside it is refused. Holds the core rather
223 // than the Computed, which the run may destroy.
224 class Computing {
225 public:
226 explicit Computing(detail::RuntimeCore& core) noexcept : _core{&core} { core.enterComputed(); }
227 ~Computing() { _core->leaveComputed(); }
228 Computing(Computing const&) = delete;
229 Computing& operator=(Computing const&) = delete;
230 Computing(Computing&&) = delete;
231 Computing& operator=(Computing&&) = delete;
232
233 private:
234 detail::RuntimeCore* _core;
235 };
236
237 template <typename F>
238 static T compute(void* fn) {
239 return std::invoke(*static_cast<F*>(fn));
240 }
241
242 // Reading is logically const; the cache, the failure and the dependency links are bookkeeping.
243 [[nodiscard]] Computed& mutableSelf() const noexcept {
244 return const_cast<Computed&>(*this); // NOLINT(cppcoreguidelines-pro-type-const-cast)
245 }
246
247 [[nodiscard]] T const& cached() const {
248 if (!_value.has_value()) {
249 throw std::logic_error{"morph::reactive: a Computed read off the owner before it had a value"};
250 }
251 return *_value;
252 }
253
254 void refuseSelfRead() const {
255 if (isPulling()) {
257 throw std::logic_error{"morph::reactive: a Computed read itself"};
258 }
259 }
260
261 T const& current() {
262 // A retry that recovers wakes observers that are not queued yet; the batch posts the flush that
263 // runs them.
264 detail::BatchScope const batch{core()};
265 pullOrThrow();
266 // Up to date now: a source that really changed has recomputed it. A failure some reader has
267 // already seen is retried, one nobody has seen yet is delivered as is, and having neither a value
268 // nor a failure (storing the last result threw) is retried as well. Each retry repeats the last
269 // run's inputs.
270 if (_error != nullptr ? !_errorUnread : !_value.has_value()) {
271 _isRetry = true;
272 forceDirty();
273 pullOrThrow();
274 }
275 if (_error != nullptr) {
276 _errorUnread = false;
277 std::rethrow_exception(_error);
278 }
279 if (!_value.has_value()) {
280 throw std::logic_error{"morph::reactive: a Computed has no value"};
281 }
282 return *_value;
283 }
284
285 void pullOrThrow() {
286 if (!updateIfNecessary()) {
287 throw std::logic_error{"morph::reactive: a Computed was destroyed by its own computation"};
288 }
289 }
290
291 bool recompute() override { // NOLINT(portability-template-virtual-member-function)
292 // Held by the run, so a computation that destroys this Computed keeps its own closure until it
293 // returns.
294 std::shared_ptr<void> const fn = _fn;
295 T (*const run)(void*) = _compute;
296 detail::TrackingFrame frame{core(), this};
297 bool const isRetry = std::exchange(_isRetry, false);
298 std::optional<T> fresh;
299 std::exception_ptr error;
300 {
301 Computing const computing{core()};
302 try {
303 fresh.emplace(run(fn.get()));
304 } catch (...) {
305 error = std::current_exception();
306 }
307 }
308 if (frame.observerDestroyed()) {
309 return false;
310 }
311 if (error != nullptr) {
312 bool const repeated = isRetry && _error != nullptr;
313 _error = error;
314 _errorUnread = true;
315 mergeSources(frame.takeSources());
316 return !repeated;
317 }
318 adoptSources(frame.takeSources());
319 bool const recovered = std::exchange(_error, nullptr) != nullptr;
320 if constexpr (kEqualityUsable<T>) {
321 if (_policy == EqualityPolicy::Auto && !recovered && _value.has_value() && *_value == *fresh) {
322 return false;
323 }
324 }
325 _value.emplace(std::move(*fresh));
326 return true;
327 }
328
329 std::shared_ptr<void> _fn;
330 T (*_compute)(void*);
331 EqualityPolicy _policy;
332 std::optional<T> _value;
333 // The latest computation's failure, or null after a success.
334 std::exception_ptr _error;
335 // Whether `_error` has not reached a reader yet.
336 bool _errorUnread = false;
337 // Whether the next run is a retry, forced by a read of an up-to-date node.
338 bool _isRetry = false;
339};
340
341/// @brief A side effect: runs once on construction, then in a flush after any source it read changed.
342///
343/// Queued Effects run shallowest scope first: an Effect a `Scope` makes carries that scope's depth, so an
344/// owner's Effects run before those of the scopes it owns. Among Effects of one depth, older ones run first.
345///
346/// An exception escaping it is a defect: reported (`detail::site::kEffectThrew`), and the flush stops.
347/// A run that throws keeps every source it has read, before and during that run, so a later change
348/// to any of them runs it again. A body may destroy its own Effect (a remount); the body's callable
349/// stays alive until the body returns, and the Effect is not touched afterwards.
350class Effect final : public detail::Node {
351public:
352 /// @brief Constructs the Effect and runs it once. Off the owner this is reported
353 /// (`detail::site::kOffOwner`) and the Effect never runs.
354 /// @tparam F A callable taking no arguments; it may be move-only.
355 /// @param runtime The runtime this effect belongs to.
356 /// @param fn The body; every signal it reads becomes a dependency.
357 /// @param depth The depth of the `Scope` that owns this Effect (`Scope::depth()`); zero for an Effect no
358 /// scope owns.
359 template <typename F>
360 requires std::invocable<F&> && std::move_constructible<F>
361 Effect(Runtime& runtime, F fn, std::size_t depth = 0)
362 : Node{runtime.core(), depth}, _body{std::make_shared<F>(std::move(fn))}, _run{&runBody<F>} {
363 forceDirty();
364 if (!bornOnOwner()) {
365 return;
366 }
367 try {
369 } catch (...) {
371 }
372 }
373
374 ~Effect() override = default;
375 Effect(Effect const&) = delete;
376 Effect& operator=(Effect const&) = delete;
377 Effect(Effect&&) = delete;
378 Effect& operator=(Effect&&) = delete;
379
380private:
381 [[nodiscard]] bool isEffect() const noexcept override { return true; }
382
383 template <typename F>
384 static void runBody(void* body) {
385 std::invoke(*static_cast<F*>(body));
386 }
387
388 bool recompute() override {
389 // Held by the run, so a body that destroys this Effect keeps its own closure until it returns.
390 std::shared_ptr<void> const body = _body;
391 void (*const run)(void*) = _run;
392 detail::TrackingFrame frame{core(), this};
393 try {
394 run(body.get());
395 } catch (...) {
396 if (!frame.observerDestroyed()) {
397 mergeSources(frame.takeSources());
398 }
399 throw;
400 }
401 if (!frame.observerDestroyed()) {
402 adoptSources(frame.takeSources());
403 }
404 return false;
405 }
406
407 std::shared_ptr<void> _body;
408 void (*_run)(void*);
409};
410
411} // namespace morph::reactive
Command run(MutationHandle< Action > const &mutation, Body< Action > const &body={}, RunOptions const &options={})
run: runs a mutation.
Definition author.hpp:2755
A derived value: computed lazily on first read, cached, and recomputed only after a source it read re...
Definition signal.hpp:168
T const & peek() const
Reads the value, bringing it up to date first, without subscribing anything.
Definition signal.hpp:212
T const & get() const
Reads the value, bringing it up to date first, and subscribes the innermost tracked run.
Definition signal.hpp:198
Computed(Runtime &runtime, F fn, EqualityPolicy policy=EqualityPolicy::Auto)
Constructs the Computed without computing it: the first read does.
Definition signal.hpp:178
A side effect: runs once on construction, then in a flush after any source it read changed.
Definition signal.hpp:350
Effect(Runtime &runtime, F fn, std::size_t depth=0)
Constructs the Effect and runs it once. Off the owner this is reported (detail::site::kOffOwner) and ...
Definition signal.hpp:361
The owner of a signal graph: every node takes one, and every operation runs on its owner.
Definition runtime.hpp:70
A source value. Reading it with get() inside a tracked run subscribes that run.
Definition signal.hpp:49
void mutate(F &&fn)
Changes the value in place and always notifies observers.
Definition signal.hpp:108
Signal(Runtime &runtime)
Constructs a value-initialised signal.
Definition signal.hpp:53
T const & get() const
Reads the value, subscribing the innermost tracked run.
Definition signal.hpp:76
Signal(Runtime &runtime, T initial, EqualityPolicy policy=EqualityPolicy::Auto)
Constructs a signal holding initial.
Definition signal.hpp:61
void set(T value)
Replaces the value and notifies observers, unless it equals the current one and the policy is Equalit...
Definition signal.hpp:90
T const & peek() const noexcept
Reads the value without subscribing anything.
Definition signal.hpp:85
A graph node: a source (Signal), an observer (Effect), or both (Computed).
Definition graph.hpp:432
std::size_t depth() const noexcept
How deeply the scope owning this node is nested; the flush runs shallower Effects first.
Definition graph.hpp:559
void trackRead() const
Registers this node as a source of the innermost tracked run, if any.
Definition graph.hpp:590
bool bornOnOwner() const noexcept
Whether this node was constructed on the owner; one that was not is refused its work.
Definition graph.hpp:587
Node(std::shared_ptr< RuntimeCore > core, std::size_t depth=0)
Constructs a node. Off the owner this is reported (site::kOffOwner) and the node is not counted towar...
Definition graph.hpp:438
void forceDirty() noexcept
Marks this node Dirty, so the next pull recomputes it.
Definition graph.hpp:598
bool isPulling() const noexcept
Whether a pull is bringing this node up to date right now. A read of the node from inside that pull c...
Definition graph.hpp:603
void mergeSources(std::vector< Node * > fresh)
Adds fresh to this node's sources without dropping any: what a failed run keeps.
Definition graph.hpp:645
void markObserversStale(Colour colour)
Marks every observer of this node colour.
Definition graph.hpp:607
bool updateIfNecessary()
Pull phase: brings this node up to date, recomputing only if a source really changed.
Definition graph.hpp:520
RuntimeCore & core() const noexcept
The runtime core.
Definition graph.hpp:583
void adoptSources(std::vector< Node * > fresh)
Replaces this node's sources with fresh, unlinking the ones no longer read.
Definition graph.hpp:618
State shared by one Runtime and every node made from it.
Definition graph.hpp:135
void leaveComputed() noexcept
Marks a Computed as done computing.
Definition graph.hpp:298
void report(char const *where) const noexcept
Reports a misuse: asserts in a debug build, or hands it to an installed owner probe.
Definition graph.hpp:167
Collects the sources one tracked run reads, and restores the previous frame when it ends.
Definition graph.hpp:367
The reactive graph's internals: node colours and links, the tracking frame, batching and the flush qu...
constexpr char const * kComputedReadsItself
A Computed read itself; the read throws std::logic_error.
Definition graph.hpp:46
constexpr char const * kEffectThrew
An exception escaped an Effect; the flush stops.
Definition graph.hpp:40
constexpr char const * kSetInComputed
A signal was written while a Computed was computing; the write is dropped.
Definition graph.hpp:44
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
EqualityPolicy
Whether a Signal or Computed compares a new value with the current one before notifying.
Definition signal.hpp:26
@ Auto
Skip a value equal to the current one, when == is usable for the type (kEqualityUsable).
Definition signal.hpp:28
@ Always
Notify on every write or recomputation, even of an equal value.
Definition signal.hpp:30
constexpr bool kEqualityUsable
Whether == is usable for T, which is what the EqualityPolicy::Auto skip needs.
Definition signal.hpp:40
Whether == is usable for T: declared, and its element types usable too.
Definition graph.hpp:77