Reactive variable in Warp9 may have a value or be empty, by default it empty
var a = new warp9.Cell();
To put a value to reactive variable you should use a "set" method
var a = new warp9.Cell();
a.set(42);
The alternative is to pass a value via constructor
var a = new warp9.Cell(42)
At any moment a variable may be set empty
var a = new wapr9.Cell(42);
a.unset()
A variable can be asked whether it has value
var a = new wapr9.Cell(42);
console.info(a.hasValue());
//> true
If a variable has value it can be gotten
var a = new wapr9.Cell(42);
console.info(a.get());
//> 42
If you try to get value of empty variable an exception
will be thrown, unless you pass an default value to the
get
console.info(new warp9.Cell().get(42));
//> 42
You can apply a function to a reactive variable to get a new reactive variable which is bound with the first with that function. If the first is updated the second will be equal to a result of the function applied to the first's value.
var a = new warp9.Cell(); // a is empty
var b = a.lift(function(a){
return a+2;
}); // b is empty
a.set(1); // a has 1, b has 3
a.set(5); // a has 5, b has 7
a.unset(); // a is empty, b is empty
There is a law for lift - for all f,x:
new warp9.Cell(x).lift(f).get()===f(x);
We can't use set & unset against variable we got as a result of lift call. Actually we can use set & unset only against reactive variables we got as a result of calling a warp9.Cell constructor.
Each reactive variable has a coalesce
method. It accepts one argument and
returns a new reactive variable which has the same value as the first one,
but if the first is empty, the second has a value equal to the argument.
var a = new warp9.Cell();
var b = a.coalesce(42); // b has 42
a.set(13); // a has 13, b has 13
a.unset(); // a is empty, b has 42
isSet
returns a reactive variable which has true when a source
has a value and false otherwise
var a = new warp9.Cell();
var b = a.isSet(); // b has false
a.set(13); // a has 13, b has true
a.unset(); // a is empty, b has false
Another way to create a variable is to call when
method. If it
is called with one argument, its argument is regarded as a filter. The filter
is executed on each value change of a source variable, if the result is true
then the created would have the same value or it would be empty otherwise.
var a = new warp9.Cell();
var b = a.when(function(a){
return a>3;
}); // b is empty
a.set(42); // a has 42, b has 42
a.set(1); // a has 1, b is empty
a.set(4); // a has 4, b has 4
a.unset(); // a is empty, b is empty
You can use a value as the function. In such case it will be compared via === with the argument.
It is possible to pass two arguments to when
method. The first
is regarded as filter, the second as transformer. The transformer is applied
to a value if the filter returns true, a result is written as a value to
a created reactive variable. If the value of source variable is changed, the
process if repeated.
var a = new warp9.Cell();
var b = a.when(
function(a) { return a>3; },
function(x) { x+1; },
); // b is empty
a.set(42); // a has 42, b has 43
a.set(1); // a has 1, b is empty
a.set(4); // a has 4, b has 5
a.unset(); // a is empty, b is empty
Such version of when(…, …)
is almost equivalent to a combination of
when(…)
and lift(…)
. We may say that there is a law, for
all f,t,cell:
cell.when(f,t)==cell.when(f).lift(t)
I say almost because instead of transformer-function we may pass a transformer-value, which yields itself on each "invocation".
var a = new warp9.Cell();
var b = a.when(42,13); // b is empty
a.set(42); // a has 42, b has 13
a.set(4); // a has 4, b is empty
The last form of when(…, …, …)
accepts three arguments: filter,
transformer and alternative transformer, which is applied if the result of
the filter is false.
var a = new warp9.Cell();
var b = a.when(
function(a) { return a>=0; },
function(x) { return x+1; }
function(x) { return x-1; }
); // b is empty
a.set(0); // a has 0, b has 1
a.set(-1); // a has -1, b has -2
a.unset(); // a is empty, b is empty
You also can pass a value instead of filter, transformer or alternative transformer, but if we omit it we may say that there a law, for all cell, f, t, a:
cell.when(f,t,a)==cell.lift(function(c) { return f(c) ? t(c) : a(c) });
The most powerful way to create reactive variables is to use
warp9.do(…)
construct. All previous ways (isSet, coalesce, lift
and when) are implement via warp9.do(…)
.
Suppose you want to define a new reactive variable which value is equal to
the sum of values of two another reactive variables. Also you want your
new reactive variable to be automatically updated when any of two others
changes. It can be done easily with warp9.do(…)
:
var a = new warp9.Cell(1);
var b = new warp9.Cell(2);
var sum = warp9.do(function(){
return a.get() + b.get();
}); // sum has 3
a.set(2); // sum has 4
b.set(-2); // sum has 0
Warp9 tracks all variable you access inside the lambda and reevaluate it each time a dependency changes.
On any invocation the lambda may change its dependency. Lets see on example:
var a = new warp9.Cell();
var b = new warp9.Cell(1);
var c = new warp9.Cell(2);
var ternary = warp9.do(function(){
return a.get() ? b.get() : c.get();
}); // ternary's only dependency is a
a.set(true); // ternary's dependencies are a,b
b.set(4);
a.set(false); // ternary's dependencies are a,c
a.unset(); // ternary's only dependency is a
I told that all form of when
are implemented via warp9.do
.
Lets see how the when(…)
may be implemented:
var cell = new warp9.Cell(42);
cell.when = function(filter) {
return warp9.do(function(){
return filter(this.get()) ? this.get() : warp9.empty();
}, this);
};
You can see that warp9.do
has two argument form, where the second
argument is an context. Another thing you haven't seen before is warp9.empty()
.
Call it inside the warp9.do's lambda to unset the created reactive value.
By default list is empty
var list = new warp9.List();
You can add values to list via constructor
var list = new warp9.List([1,2,3]);
Or using an add
method
var list = new warp9.List();
list.add("Warp9");
list.add("React");
You can access to the values of list via get
var list = new warp9.List();
list.add("Warp9");
list.add("React");
console.info(list.get());
// ["Warp9", "React"]
Method add
returns an id, which can be used to
remove as element.
var list = new warp9.List();
var warpId = list.add("Warp9");
var reactId = list.add("React");
console.info(list.get()) ;
// ["Warp9", "React"]
list.remove(reactId);
console.info(list.get());
// ["Warp9"]
Often it is needed to have an id as a part of an element, to
achieve that you can pass a function to add
method
which will be executed by add method, the function will receive
an id and the returned value will be inserted to the list.
var list = new warp9.List();
list.add(function(id) {
return {id: id, name: "Warp9"};
});
console.info(list.get());
// [{id:15, name: "Warp9"}];
There is an another method to remove elements beside remove
-
removeWhich
. It removes elements by predicate.
var list = new List([1,2,3]);
list.removeWhich(function(x){
return x<2;
});
// list contains 2,3
Just like js's array warp9.List has forEach method
var list = new warp9.List(["Warp9", "React"]);
list.forEach(function(x){
console.info(x);
});
// Warp9
// React
remove
, removeWhich
and forEach
execute immediately and once, they do not have long reactive effect like
Cell's lift.
By the way List has its own version of lift, you may think of it as a reactive version of map.
var a = new warp9.List();
var b = a.lift(function(x) { return 2+x; });
var id1 = a.add(1);
// a has values [1], a has values [3]
var id2 = a.add(2);
// a has values [1, 2], a has values [3, 4]
a.remove(id1);
// a has values [2], a has values [4]
You can't call add
, remove
, removeWhich
on list you got as a result of lift
method.
Here I mentioned that support of reactive lists in Knockout and ReactiveCoffee is very limited if we compare it to Warp9. Let explore it. For a start we calculate the sum of elements in a list.
var list = new warp9.List();
var sum = list.reduce(0, function(a,b){
return a+b;
}); // reduce returns a reactive variable
list.add(41); // sum has 41
list.add(1); // sum has 42
As we saw the sum reflects the changes to the list. Lets now calculate the count of the list elements.
var list = new warp9.List();
var count = list.lift(function(x){
return 1;
}).reduce(0, function(a,b){
return a+b;
});
var id41 = list.add(41); // count has 1
list.add(1); // count has 2
list.remove(id41); // count has 1
This code does what it should do, but it is not optimal. For example it creates an intermediate list. Warp9 provides an api to avoid it.
var list = new warp9.List();
var count = list.reduce(0, function(a,b){
return a+b;
}, {
wrap: function(x) { return 1; }
});
var id41 = list.add(41); // count has 1
list.add(1); // count has 2
list.remove(id41); // count has 1
wrap
is executed for each element and the result is passed
to further reduce logic.
Since we start talking about performance, what is the complexity of updating the "reduced" variable during list manipulation?
It it were Knockout the complexity would be O(n) in term of list length, because a reactive list in Knockout is an reactive variable whose value is an array and if you want to do a reactive aggregation you should subscribe and reevaluate aggregation on each change.
It Warp9 reactive list is an first class entity. Such approach allows to do some optimisation. For example, a complexity of updating a reduced value during list insert/remove is O(ln n).
But O(ln n) complexity is still too high for calculating count or sum during each list update, so Warp9 provides api to do it in O(1). If you see to Warp9's reduce method implementation, you will find something similar to
BaseList.prototype.reduce = function(identity, add, opt) {
return this.reduceMonoid({
identity: function() {return identity; },
add: add
}, opt);
};
If you are familiar with functional programming or abstract algebra
you may guess that if we have reduceMonoid
we may have
reduceGroup
. If you do so, you are right. So we can
rewrite our example with sum from reduceMonoid
(reduce
)
to reduceGroup
var sum = new list.reduceGroup({
identity: function() { return 0; },
add: function(a,b) { return a+b; },
invert: function(x) { return -x; }
});
And the complexity falls from O(ln n) to O(1). The same we can do with count example
var count = new list.reduceGroup({
identity: function() { return 0; },
add: function(a,b) { return a+b; },
invert: function(x) { return -x; }
}, {
wrap: function(x) { return 1; }
});
Lets implement a bit sophisticated aggregator. Suppose our list contains
boolean values and we want to do logical AND operation over a list's
values. Since if know an aggregated value and a element to remove from
aggregation we can't calculate a new aggregated value it seems that we
should use reduceMonoid
var all = new list.reduceMonoid({
identity: function() { return true; },
add: function(a,b) { return a & b; }
});
But if we think we may come to idea to count "true" and "false" value
independently. It allows us to calculate a new aggregated value if
we know the last and removing element, so we can use reduceGroup
var all = new list.reduceGroup({
identity: function() { return [0,0]; },
add: function(x,y) { return [x[0]+y[0], x[1]+y[1]]; }
invert: function(x) { return [-x[0], -x[1]]; }
}, {
wrap: function(x) {
return x ? [1,1] : [0,1];
}
}).lift(function(x){
return x[0]==x[1];
});
This code is much better since it is O(1) compared to O(ln n), but it still sucks because it needs an intermediate variable (we call lift on it), lets fix it, Warp9 provides a api for that
var all = new list.reduceGroup({
identity: function() { return [0,0]; },
add: function(x,y) { return [x[0]+y[0], x[1]+y[1]]; }
invert: function(x) { return [-x[0], -x[1]]; }
}, {
wrap: function(x) {
return x ? [1,1] : [0,1];
},
unwrap: function(x) {
return x[0]==x[1];
}
});
The best part about reduceMonoid
and reduceGroup
is that they take into account if a list's element is a reactive variable.
It means you can put a reactive variable to a reactive list, do aggregation
and get a reactive variable which will be updated when the list changes or
the reactive variable put to list changes. See an example
var list = new warp9.List();
var it1 = new warp9.Cell(0);
var it2 = new warp9.Cell(1);
var sum = list.reduce(0, function(a,b){
return a+b;
}); // sum has 0
var itId1 = list.add(it1); // sum has 0
var itId2 = list.add(it2); // sum has 1
it1.set(5); // sum has 6
it2.set(2); // sum has 7
it1.unset(); // sum is empty
list.remove(it1); // sum has 2
You can see that if a reactive variable is empty in a list, then a reduced value is empty too. Sometimes it is useful to consider an empty variable as an identity element, you can do it with ignoreUnset option
var item1 = new warp9.Cell();
var item2 = new warp9.Cell(1);
var list = new warp9.List([item1, item2]);
var sum = list.reduce(0, function(a,b) {
return a + b;
},{
ignoreUnset : true
}); // sum has 1
item1.set(3); // sum has 4
Reactive model (aka observer pattern, publish-subscribe pattern, etc.) is great, it solves a lot of problems, for example it allows to build composable applications, but you should use it carefully, because it has its own pitfalls. One of them is memory leaks. If we look through a wiki article about memory leaks, we'll see:
To prevent this (memory leaks), the developer is responsible for cleaning up references after use ... and, if necessary, by deregistering any event listeners that maintain strong references to the object
This problem is actual for most observer pattern implementations. Martin Fowler mentions and a lot of articles are dedicated to it.
I believe when there are a lot problems of particular kind with code which uses a library this is the library's problem (even there is a workaround in the docs). So if you are designing a library you should think how to prevent errors or at least make them visible. I followed this principle when designed Warp9. With Warp9 your code has a leak related to reactivity if and only if you forget to remove subscription to variable you have subscribed to. It means you have an explicit way to test your code for memory leaks and a rule to avoid them.
To subscribe to a reactive variable (Cell or List) you should call onChange
method
and pass a handler (a function) as an argument. This function will be called once after subscription
and then each time the variable changes. On each invocation handler receives an object it subscribed to
and an event.
To remove a subscription you should call a method returned by onChange
.
When you deal with Cell, you can ignore the event and use cell's methods to recognise its state.
var cell = new warp9.Cell();
var dispose = cell.onChange(function(cell, event){
if (!cell.hasValue()) {
console.info("unset: event = " + JSON.stringify(event))
} else {
console.info("set: value = " + cell.get() + ", event = " + JSON.stringify(event));
}
});
cell.set(1);
cell.unset();
dispose(); // <-- to prevent memory leak
cell.set(2);
This yields to console:
unset: event = ["unset"]
set: value = 1, event = ["set",1]
unset: event = ["unset"]
If your reactive object is variable (Cell) you have another methods to subscribe:
.onSet(handler)
and .on(obj, handler)
. Those methods also
returns a method that removes the subscription. If you subscribe via onSet
to a variable then your handler may be called once after subscription (if the variable
has value) and then each time the variable is set, so you can be sure that it has value.
on(obj, handler)
has event stronger condition, your handler may be called once
after subscription (if the variable has value equal to obj) and then each time when
the variable is set to a value equal to obj.
Lets exam List's events
var list = new warp9.List();
var dispose = list.onChange(function(list, event){
console.info(JSON.stringify(event));
});
var key42 = list.add(42);
var key13 = list.add(13);
list.remove(key13);
list.setData([1910, 1948]);
dispose(); // <-- to prevent memory leak
list.add(1900);
This yields to console something similar to:
["reset",[]]
["add",{"key":34,"value":42}]
["add",{"key":35,"value":13}]
["remove",35]
["reset",[{"key":36,"value":1910},{"key":37,"value":1948}]]
After subscription a handler will be called with "reset" event which argument reflects the content of the list at that moment (for each value a key is also provided). When an element is added the handler will be called with "add" event, also when an element is deleted it will be call with "remove" event, which argument is a key of removed element. The events are ordered, it means you can take the value of the last "reset" event, apply all subsequent "add" & "remove" events and get the current value of the list.