Skip to content

Commit

Permalink
Finish basics of quarve book
Browse files Browse the repository at this point in the history
  • Loading branch information
enigmurl committed Jan 9, 2025
1 parent 7cf0559 commit 0f41673
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 20 deletions.
10 changes: 5 additions & 5 deletions src/cheat_sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ TextField::new(text.binding());

Text Button
```rust
button("Label", |s| {...})
button("Label", |s| { /* action */ })
```

\*There are many controls that one would expect from a UI library that are yet to
Expand All @@ -56,11 +56,11 @@ vstack()
.push(ivp1)
.push(ivp2)

// binding
binding.binding_vmap(|content, s| text(content.to_string());
// vector binding to vstack
binding.binding_vmap(|content| text(content.to_string());

// signal (slow)
signal.sig_vmap(|content, s| text(content.to_string());
// vector signal to vstack (slow)
signal.sig_vmap(|content| text(content.to_string());
```

## Modifiers
Expand Down
317 changes: 312 additions & 5 deletions src/state.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,327 @@
# State
The way state is done is extremely important to the (theoretical at least)
efficiency of Quarve. It is partially inspired by Group Theory.
This lesson is also a bit long. The important parts are the state lock,
signals, stores and bindings. You can come back to the other parts
after finishing the rest of the lessons.

**Remark:** Some of the terminology of the state system comes from Group theory
(e.g. Word, \[group\] Action).

## State Lock
Most native UI libraries have some operations that can only be performed on the main
thread. Unfortunately, this is often unchecked completely or checked at runtime.
Quarve uses Rust's ownership mechanics to check most of this at compile time.

The idea is that there is a global state lock that only one thread at a time can
hold. Any thread which wants to update or read from the state must acquire the
state lock (often abbreviated 'slock'). To prove that the caller of a certain
function must be holding this lock, you can simply add as an argument
`s: Slock`, which is a marker that can only be acquired if you are in fact the
holder.

There are two types of slocks.
1. `Slock<MainThreadMarker>` which is abbreviated `MSlock`.
2. `Slock<AnyThreadMarker>` which is abbreviated `Slock`.
Note that you can freely convert slocks of the first kind to the second kind
using `.to_general_slock`, which is occasionally necessary. However, converting
the second kind to the first kind fails if you're not on the main thread.

Consequently, many methods in the Quarve library take as argument
either `Slock<impl ThreadMarker>`, denoting only the state lock is necessary,
or `MSlock>`, denoting the state lock is necessary and this function
must be called on the main thread.

**Advanced**: For simple applications, you likely only pass the slock marker
and delegate actually acquiring the lock to the caller of your function
(which will typically be quarve).
However, to explicitly acquire the state locker yourself (as is necessary
for worker threads), you can use the `slock_owner` function
From here, you can get the actual state lock markers using the `marker` function.

**Remark:** Conventional wisdom argues against having a global state lock
as it may reduce parallelism. However, we believe that in the vast majority of cases
only the main thread will hold the lock and worker threads
only need to hold it for a very short time when they perform
the state transformation.
Also, the state lcok greatly reduces race conditions as well.
Finally, we avoid the cost of constantly acquiring and releasing mutexes.
Hence, we believe that in many cases the tradeoff is worth it.

## Signals

### Signal Listeners
Signals are simply values that change over time. Moreover, receivers
can get notified after the value is updated. Note that `Signal`
is a trait, and its value is the associated type Target.

For instance, quarve provides `clock_signal` function that is simply
a signal that continuously increases with time. Here's an example of
how we can apply a listener.
```rust
fn run(s: MSlock) {
let sig = clock_signal(s);
sig.listen(|val, s| {
println("Time is {}", val);
true
}, s);

// you can also query the current value of a signal
// using the borrow method
// note that this requires the state lock as well
let curr = *sig.borrow(s);
}
```

Notice that signal listeners are given the state lock as a parameter,
and you can only add listeners if you have the state lock.
Listeners also should return a boolean value denoting whether they
want to continue listening or should be dropped.
In the above example, we want to listen forever so we always
return `true`. In many cases, you return false once a weak
reference can no longer be upgraded.

By default, `listen` gives a notification for all possible updates,
even when the 'new' value is the same as the old one.
If you would only like to get notifications, you can use
`diff_listen` instead (this requires the target to implement `PartialEq`).

## Store
The heart of state is a Store.
The heart of state is a `Store`. Conceptually, a store simply
stores a particular value and notifies to its observers whenever it changes.
In this aspect, it's a signal. However, unlike a signal, a `Store` can also
be changed.

In the simplest case, this change is done by manually overwriting the current value.
```rust
let store = Store::new(0);
// notice the usage of the state lock
// Here, we apply the SetAction::Set to modify the current value
store.apply(Set(4), s);

// you can acquire a signal from a store explicitly
// sig: impl Signal<Target=i32> + Clone
let sig = store.signal();

// or... you can add listeners directly to the store
store.listen(|val, s| {
...
true
}, s);
```

The advantage of having the action be an actual struct passed in
(as opposed to e.g. some function on the object)
is that now observers can see the exact transformation that was
applied, which can be surprisingly useful.

For many types, such as integers and floats, the 'action' of how you
change the value is simply setting it. For instance, the action for a vector
is a sequence of inserts or removals.
```rust
let store = Store::new(vec![Store::new(false)]);

// for store of vectors,
// it may not be obvious why we need the inner store
// (in addition to the outer one)
// you can think about it as the top level store lets us observe
// if elements are added or deleted, but doesn't know if they're modified
// the individual sub stores allow observes to see if any particular
// element of the vector is modified
store.apply([
VecActionBasis::Insert(Store::new(false), 0),
VecActionBasis::Remove(1)
], s);
```

Again, the advantage is that now an observer is not only notified that
the vector will change, but they can see exactly *how* it will be changed
by looking at the action. This can, for example, figure out how to efficiently
undo the action or possibly mirror the action onto another vector.

## Binding
In particular, in addition to being a signal, stores also implement
the `Binding` trait. The `Binding` trait is subtrait of `Signal`,
further specifying that you must be able to change the current
value using an action (as was seen above), and add an `action_listener`.

An action listener gets notified every time a store *is about to* change
(in contrast, regular listeners are notified afterwards). It is given
the current value, the action, and the state lock.
```rust
let store = Store::new(vec![false, true]);
// see below for why the different syntax wrt to signal
// b: impl Binding<Filterless<Vec<bool>> + Clone
let b = store.binding();

// here we call action listen on the binding
// but you could've just called it on the store itself
b.action_listen(|val, action, s| {
/* act appropriately */

// again, return a value based on whether we want to continue
// listening
true
}, s);

```

Finally, we note that `Store` implements `Binding` but does not
implement `Clone`. If you would like a cloneable binding,
explicitly call the `binding` method on the store.
(likewise, for a cloneable signal, use the `signal` method).

### Action Listen
Also note that Stores are internally arc-ed. In particular,
even if the original `Store` is dropped but one of the bindings
is still alive, you can still use the bindings as they hold strong references.
You can instead call `weak_binding` to only hold a weak reference.

**Note:** There is a slight asymmetry between `impl Signal<Target=T>`
and `impl Binding<Filterless<T>>`.
The filterless part is basically for something
that didn't really end up getting used but is still a part of quarve.
It may be used in the future, though.

## Store Container
In any actual UI application, the main state is not simply a single
store, but rather a collection of them. We would like to organize such stores
into containers. The `quarve_derive` crate provides a utility to automatically
derive StoreContainer.

```rust
#[derive(StoreContainer)]
struct ApplicationState
// non stores should be ignored
#[quarve(ignore)]
app_name: String,

count: Store<usize>,
shopping_cart: Store<Vec<Item>>,
// store containers can hold other store containers
// (assume that UserState is some other StoreContainer)
user: UserState
}
```

In addition to grouping together state, StoreContainers provide
the the functionality of adding a listener to see when
*any* contained store changes. As we'll soon see, StoreContainers also are
used for adding undo/redo support.

```rust

let a = ApplicationState::new(/* snip */);
// note that only one subtree general listener
// can ever be present at any given time
a.subtree_general_listener(|s| {
// note that we are not explicitly told which
// sub container is changed
true
}, s);
```

Quarve also provides `StoreContainerSource` and `StoreContainerView`
to add arc funtionality to store containers, if needed.

## Undo
Quarve makes it easy to add undo support. Place all stores that
should be undoable under some `StoreContainer`. Then, simply
create an `UndoManager` and call `mount_undo_manager` on
the target IVP. Whenever the IVP is mounted on the view
hierarchy (i.e. visible), you will be able to undo any action.

```rust
fn root(&self, env: &<Env as Environment>::Const, s: MSlock) -> impl ViewProvider<Env, DownContext=()> {
let a = ApplicationState::new(/* snip */);
// specify the store container
let u = UndoManager::new(&a, s);

ivp
.mount_undo_manager(u)
.into_view_provider(env, s)
}
```

**TODO (advanced) mention undo grouping, undo bucket and history_elide**

## Derived Store
**TODO add more details here**
However, sometimes we want to explicitly exclude a Store
from undo operations. This usually happens because a Store is not
actually independent of other stores, but is instead a function of them
(i.e. its value is 'derived' from other stores).
Concretely, let's suppose that we have two stores. One is a counter,
and another is a double counter that always
(toy example, you can imagine that actual conversion
to be much more complex).
```rust
let a = Store::new(0);
let b = Store::new(0);

// always update b
let binding = b.binding();
a.listen(move |val, s| {
binding.apply(Set(2 * val), s);
true
}, s);
```

In such a case, let's suppose somewhere that `a` is updated
to `1`. Following this, `b` will be updated to `2`.
When we request an undo, we will revert the most recent action,
so `b` will go back to `0`. Then, we will set back `a` to `0`.
Since `a` was changed, all listeners will be invoked,
so `b` will again be set to `0`. This last operation is redundant,
and we could've simply gotten away with never undoing
the action for b in the first place. Hence, it makes more sense
to mark `b` as a DerivedStore that should not partake
in undo operations.

You can imagine this becomes much more problematic when `b` is
trying to mirror the actions of `a` (that aren't simply SetActions,
think of a vector).
In this case, applying the inverse action twice will lead
to incorrect behavior!


## Stateful
How does quarve know which action type shoul be used for a given store?
The answer is that any item used in the store must implement
the `Stateful` trait. Part of the `Stateful` trait is
setting the associated type to the natural action.

Hence, if you want to create a store of some custom type,
you must implement `Stateful` for the your custom type.
For many cases, you can get away by setting the action
to be `SetAction`. If necessary, you can instead create
your own action and implement the `GroupAction` trait
for it.

\* Technically, it need not be a 100% proper
group action, but it's nearly there.

## IntoAction
Sometimes, we may want to perform a modification that
isn't naturally thought of as the standard action for
some Store. For example, let's suppose we have a counter.
It's quite annoying to use a set action to increment
since we must first borrow the value.

Hence, we note that `Binding::apply` actually accepts
a value that implements `IntoAction`, rather than solely
the standard action of the `Store`. Consequently, we can create
an `IncrementAction` struct that implements `IntoAction` and
be converted into a `SetAction`. Crucially, note that in the below
signature, we are allowed to look at the target value when performing
the conversion so that we may simply return `SetAction::Set(1 + *target)`.

```rust
fn into_action(self, target: &T) -> A;
```

In fact, `NumericAction::Incr` exists in the quarve library
and does exactly this.
You can feel free to create your own too though, as you see fit.

Another use case is to simply give syntatic sugar for actions
that are cumbersome to type. For instance,
`[VecActionBasis; N]` implements `IntoAction` so that
you can type `[VecActionBasis::insert(...)]` instead of
`Word::new(vec![VecActionBasis::insert(...)])`.
Loading

0 comments on commit 0f41673

Please sign in to comment.