flo_binding 1.0.0

Declarative binding library for Rust
Documentation

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 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

# use flo_binding::*;
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
# assert!(plusone.get() == 2);
number.set(2);                  // 'Changed!'
println!("{}", plusone.get());  // 3
# assert!(plusone.get() == 3);
lifetime.done();

number.set(3);                  // Lifetime is done, so no notification
println!("{}", plusone.get());  // 4
# assert!(plusone.get() == 4);