Understate
A simple state manager.
This was inspired by Redux along with another old project of mine.
Understate aims to be similar to Redux, but with some parts abstracted out of the core library using higher-order functional concepts.
In addition, Understate provides a mechanism for indexing and retrieve states by id.
Understate does not enforce immutability. However, using immutable objects as values for state has number advantages related to performance, correctness, and reasonability. Consider using it in conjunction with a library such as Immutable.
About
Understate works by creating objects that ingest mutator functions to update their internal state. Wait, what?!
...Okay, let's start over... maybe if we just jump right into it...
Basic Usage
When you first create an Understate object, it has an initial internal state that can be accesses via the "get" method.
;var console;var state = ;state;//undefined
You can also pass an initial value when creating a Understate object.
var state = initial: 0;state;//0
You can update the internal value by passing a mutator (See Below) function to the "set" method.
var x + 1;var state = initial: 0;state;//0state;//1
You can subscribe to updates with the "subscribe" method.
var state = initial: 0;state;state;//1state;//2state;//3
You can unsubscribe to updates by calling the "unsubscribe" method on the object returned by the subscribe method.
var state = initial: 0;var unsubscriber = state;state;//1state;//2unsubscriber;state;//(Nothing logged)
Indexation
Understate objects track their state internally.
Each Understate object associates an id with it's value whenever it's value us update. This can be accessed from the "id" method.
var state = initial:0;;//*<ID>state;//*<ID(Different)>
Passing a truthy "index" config option to the "set" function will cause its id to be passed as a second argument to the function passed to subscribe.
var console;var state = initial:0;state;state;//*<ID>:0state;//*<ID>:1state;//*<ID>:2
Passing a truthy "index" config option to the "set" function will also cause the Understate object to internally index it's state by id. This id can later be used to access any indexed states by passing it to the "get" method.
var state = initial:0;state;state;//0 (After 5 seconds)state;//1 (After 5 seconds)state;//2 (After 5 seconds)
Passing a truthy index option to the constructor will cause the set function to automatically index values.
var console;var state = initial:0 index:true;state;state;//*<ID>:0state;//*<ID>:1state;//undefined:2
If not already indexed, you can index the current state by passing a truthy argument to the id method.
var state = initial:0);state;var id = stateidtrue;state;//1
Indexation is a good reason to consider immutability in you application. Using mutators (below) that return modified copies of your state without modifying the original ensures that each id points to a uniquely identifiable object.
Mutators
Mutator functions should be pure functions (they have no side effects) that take in a state and return an updated copy of that state without modifying the original (they respect immutability). With that said, these ideas are pretty much programmatically unenforceable, so if you wish to follow this convention, you'll have to take special care to enforce these properties upon your code yourself.
Signature
A mutator should have the following function signature:
{/*some combination of closure and "state"*/};
Example
This mutator returns the state incremented by 1
var state + 1;
Redux Comparison
Setting state using a mutator function in Understate
Understate#;
Setting state using an action object in Redux
Redux#store;
Builders
Since a mutator function only takes in a state, any modifications that are made to it must be based on its closure.
We can take advantage of this by creating mutation builder functions that takes, as arguments, a set of parameters and return mutators that use the parameters in its closure.
Signature
A builder function should have the following function signature:
{/*some combination of closure, "parameters", and "state"*/};//OR /*<Mutator>*/;
Example
var x + y;var increment = ;//This is equivalent to the increment function defined above// adder(y) = y => x => x + y;// adder(1) = x => x + 1;
Redux Comparison
We can see that a builder function and a reducer function from Redux are very similar.
Builder Function
newState
Reducer Function
newState
If you were to reverse the parameters in a reducer...
newState
and then Schönfinkel it
newState
you'd end up with a builder that takes an "action" as its only parameter
Initialization with Constant Function Builders
It's often useful to set a state rather than modify it. In this case, we can use a function that returns a constant.
var 1;var state = ;state;state;//1state;//1state;//1
We can create constant function builders as well.
var a;var one = ;//This is equivalent to "one" defined above.var state = ;state;state;//1state;//1state;//1
Using Builders
Using different types of builders allows us to elegantly express how we modify an application's state.
//CounterApplication.js;var console;//Buildersvar a;var b + a;//Mutatorsvar zero = ;var increment = ;//Appvar counter = ;counter;counter;//0counter;//1counter;//3
//messageApplicatiopn.js;var console;//Buildersvar a;var messages;var {; return message};//Mutatorsvar empty = ;//Appvar messages = ;messages;messages;//[]messages;//['Hello']messages;//['Hello', 'there']messages;//['Hello', 'there', 'John.']
Decorators
Redux has a concept of middleware used to intercept objects and preform actions such a logging.
Rather, we can simply design our mutators to preform these actions.
//messageApplicatiopn.js;var {console; return value};//Buildersvar { console; return a};var messages;//Mutatorsvar empty = ;//Appvar messages = ;messages;//'Setting constant:'messages;//'Hello received.'messages;//'there received.'messages;//'John. received.'
Decorators take in functions return similar functions with enhanced functionality. They should take a function as one of it's arguments and return a function with the same signature. We apply them to builder functions.
//messageApplicatiopn.js;//Decoratorsvar { console; return target;}//Buildersvar constant = ;var addMessage = ;//Mutatorsvar empty = ;//Appvar messages = ;messages;//'Setting constant:'messages;//'Hello received.'messages;//'there received.'messages;//'John. received.'
Note: ECMAcript 8 (2017) has a similar new language feature, also called "decorators", that work in a similar way, but can only be applied to class methods.
Routers
The final piece of the puzzle is the router. It's job is to take "action" from another component in the application, and return a mutator function to be applied to the current state. It does this by selecting a builder based on an "action" and extracting parameters from that "action".
Strictly speaking, routers are builders, as they take in a parameter, "action", and return a mutator function. They are still; however, a useful abstraction when it comes to deciding how to handle updates.
Signature
A router should have the following signature:
{/*some combination of closure, "action parameters, and "state"*/};//OR /*<Mutator>*/;
Example
This is an application that uses a sample router.
//File: sampleRouters.js //Schema - Not strictly necessary, but helpful// {// builder : <String>,// parameter : <Number>// }// //Buildersvar b + a;var b - a;var a; var { return actionbuilder;}var { return actionparameter;} //Router Implementation: Dictionaryvar builders = ;//Available mutation buildersbuilders;builders;builders;var { var builder = builders; return builder ? : _;}; //Router Implementation: Switch Statementvar { var builder; return ;}; mapRouter switchRouter
//File: application.js;;var state = initial:0;state;var state;;
//runner.js;var actions = builder : 'add' parameter : 1 builder : 'subtract' parameter : 2 builder : 'reset' parameter : 0 ;var action;whileaction = actions ;//1//-1//0
Asynchronous Mutators
You can modify an Understate instances at creation to take asynchronous mutators by passing a truthy "asynchronous" flag to the config function. Like normal (synchronous) mutators these functions take a state as an argument. Instead of returning a modified state; however, they return a promise resolved with the modified state.
Note: We can pass an "asynchronous" config option to "set" method to temporarily override the "asynchronous" flag
var console;//Buildersvar a;var { ifMath < 025 return ; return ;};//Mutatorsvar empty = ;//Appvar messages = asynchronous:true;messages;messages;messages ;//[]//['Hello']//['Hello', 'there']//['Hello', 'there', 'John.']//OR//[Error: Simulated Async Failure]
Redux Comparison
Actions
Actions are similar, but are less flexible in Redux
Setting state using an "action" -- Here, the "action" mustn't be of any specific format -- its schema is defined by how the router interprets it
Understate#;
Setting state in Redux using an action -- Here, an action is a loosely formatted JSON object with a mandatory "type" attribute
Redux#store;
Reducers
Routers/builders are essentially reducers from Redux that have been abstracted out of the core library.
Recall the function signature a router:
{/*some combination of closure, "action parameters, and "state"*/};
This is essentially the same signature as a Redux reducer, who's first had it's parameters reversed, and then had Schönfinkeling applied.
newState
Application Programming Interface
This API is written for ECMASCRIPT 6 (2015). It makes no assumptions about the running environment of the application.
Import
;
Consturor -- new Understate({initial:any=undefined, index:boolean=false, asynchronous:boolean=false});
Create a new Understate instance
var state = ;
Create a new Understate instance with an initial value
var state = initial:/*sone initial value*/;
Create a new Understate instance that indexes state by default
var state = index:true;
Create a new Understate instance that expects asynchronous mutators.
var state = asynchronous:true;
Instance Methods
These are methods attached to an instance. For this section, you may assume "state" is an available instance of Understate.
Understate#set(mutator:function, {index:boolean=instance#index, asynchronous:boolean=instance#index});
Update the internal state of an Understate instance with a mutator (see above) function.
var '"' + String + '"';state;
Update the internal state of an Understate instance with a mutator function, index it, and pass it's id along with state in the promise resolution.
var '"' + String + '"';state;
Update the internal state of an Understate instance with an asynchronous mutator function, index it, and pass it's id along with state in the promise resolution.
var ;state;
Understate#s(mutator:function, {index:boolean=instance#index, asynchronous:boolean=instance#index});
Same as Understate#set (See above), but returns the original object for chaining.
state ;
Note: There is no guarantee about the order in which these chained methods are executed.
Understate#id();
Get the id of the current state.
stateid;
Note: this method returns the id directly and not a promise.
Understate#id(id:boolean);
Get the id of the current state and also index it if not already indexed.
stateidtrue;
Note: this method returns the id directly and not a promise.
Understate#get();
Retrieve the current state.
state;
Understate#get(index:string);
Retrieve an indexed state by id.
state;
Understate#subscribe(subscriber:function);
Subscribe to changes in a state
state;
Understate#subscribe(subscriber:function).unsubscribe();
The object returned by "subscribe" is linked to the original via prototype-chain. Methods called on the original will affect the new object and vice-versa. In addition, the returned object has an "unsubscribe" method that cancels further updates from the original function passed to "subscribe".
state;
Subscribe Implementation Notes
The current implementation uses tracks subscriptions using a Set, resulting in a few "gotchas":
Uniqueness
The following would result in multiple subscriptions:
var console;state;state;state;
while the following will only result in one
var consolestate;state;state;
as each 'log' is the same object.
Order
There is no guarantee as to the order in which subscriptions are called.
state;state;state;//(Might Log: 'Does this happen first?' 'Or does this?')