[−][src]Crate flo_binding
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()); // 4
Modules
binding_context |
Structs
BindRef | A |
Binding | Represents a thread-safe, sharable binding |
ComputedBinding | Represents a binding to a value that is computed by a function |
FollowStream | Stream that follows the values of a binding |
StreamBinding | Binding that represents the result of binding a stream to a value |
Traits
Bound | Trait implemented by something that is bound to a value |
Changeable | Trait implemented by items that can notify something when they're changed |
MutableBound | Trait implemented by something that is bound to a value that can be changed |
Notifiable | Trait implemented by items with dependencies that need to be notified when they have changed |
Releasable | Trait implemented by an object that can be released: for example to stop performing an action when it's no longer required. |
Functions
bind | Creates a simple bound value with the specified initial value |
bind_stream | Uses a stream to update a binding |
computed | Creates a computed value that tracks bindings accessed during the function call and marks itself as changed when any of these dependencies also change |
follow | Creates a stream from a binding |
notify | Creates a notifiable reference from a function |