Crate flo_binding
source · [−]Expand description
flo_binding, a data-driven binding library
flo_binding is a library of types intended to help store state in interactive
applications. A binding is a mutable value that can notify observers when it
changes, or can supply its values as a stream. This is intended to make it easy
to build ‘data-driven’ applications, where updates to model state automatically
drive updates to the rest of the application.
This method of propagating state is sometimes called ‘reactive programming’,
and has also been called ‘parametric programming’ in the past: it’s most notable
for being the methodology behind spreadsheets and CAD applications before its
recent rediscovery in web applications. flo_binding prefers the term
‘data-driven’ as it’s a more plain description of how changes are pushed through
an application using the library.
The advantage of using a binding library like this over a standard event-driven architecture is that it’s not necessary to send events or manually update state when updating the application data model. Bugs are reduced because it’s very much harder for a dependent state update to go missing as this library can automatically associate.
There are three main types of binding provided by the flo_binding library:
A normal binding, created by bind(value) can be updated to a new value by
calling set(). These are useful for representing state that is directly
manipulated by the user.
A computed binding, created by compute(move || { some_func() }). When a
computed binding accesses the value of another binding by calling get(), it
will automatically monitor it for changes and indicate that it has changed
whenever that value changes. This makes it easy to create chains of dependent
bindings.
A stream binding, created by bind_stream(stream, initial_value, |old_value, new_value| { /* ... */ });.
This makes it possible for a binding to update its values in response to a
stream of events, such as might be available from a UI framework.
To make using computed bindings easier to generate, cloning a binding creates
a reference to the same piece of state. ie: let a = bind(1); let b = a.clone();
will give a and b a reference to the same value, so a.set(2); b.get() will
return 2.
Event-driven architecture
The when_changed() function can be called to register a function that is called
whenever a binding’s value becomes invalidated. Once invalidated, a binding
remains invalidated until it’s read with get(), which avoids computing values
for computed bindings that are never used. This can be used to integrate with
traditional UI frameworks.
Calling when_changed() will return a lifetime object: the event will stop firing
when this object is dropped (or when done() is called on it). keep_alive() can
be used to keep the event firing for as long as the binding exists if necessary.
The event-driven approach to application design has many disadvantages: the need to
manage event lifetimes is one, and another important one is the need to pass the
actual bindings around in order to attach events to them, so flo_binding provides
an alternative approach.
Stream-driven architecture
A superior alternative to the traditional OO-style event-driven architecture is to take a stream-driven approach. Unlike events, streams manage their own lifetimes and can be combined or split, and it’s possible to pass around a stream without also providing access to its source, which is a property that can be exploited to produce a less interdependent application. FlowBetween, the application that flo_binding was developed for is an example of a stream-driven application.
Calling follow(binding) will generate a stream of updates from a binding. This
can be used to update part of a user interface directly. It can also be used as
an input for bind_stream() to create more complicated update rules than are
allowed by a simple computed binding.
The companion library desync provides the pipe() and pipe_in() functions
which can be used to schedule events generated by a stream.
Example
let mut number = bind(1);
let number_clone = number.clone();
let plusone = computed(move || number_clone.get() + 1);
let mut lifetime = plusone.when_changed(notify(|| println!("Changed!")));
println!("{}", plusone.get()); // 2
number.set(2); // 'Changed!'
println!("{}", plusone.get()); // 3
lifetime.done();
number.set(3); // Lifetime is done, so no notification
println!("{}", plusone.get()); // 4Modules
Structs
A BindRef references another binding without needing to know precisely
what kind of binding it is. It is read-only, so mostly useful for passing
a binding around, particularly for computed bindings. Create one with
BindRef::from(binding).
Represents a thread-safe, sharable binding
Represents a binding to a value that is computed by a function
Stream that follows the values of a binding
A rope binding binds a vector of cells and attributes
A rope binding binds a vector of cells and attributes
A rope stream monitors a rope binding, and supplies them as a stream so they can be mirrored elsewhere
Binding that represents the result of binding a stream to a value
Traits
Trait implemented by something that is bound to a value
Trait implemented by types that represent a rope binding
Extension methods that can be applied to any bound rope
Trait implemented by items that can notify something when they’re changed
Trait implemented by something that is bound to a value that can be changed
Trait implemented by items with dependencies that need to be notified when they have changed
Trait implemented by an object that can be released: for example to stop performing an action when it’s no longer required.
Trait implemented by something that is bound to a value
Functions
Creates a simple bound value with the specified initial value
Uses a stream to update a binding
Creates a computed value that tracks bindings accessed during the function call and marks itself as changed when any of these dependencies also change
Creates a stream from a binding
Creates a notifiable reference from a function