Awesome
recalc
"[T]hus, the external world would only have a triggering role in the release of the internally-determined activity of the nervous system." --Humberto Maturana
"Should that function take the whole world as input and return a brand new world as output? Why even use functional programming, then?" --James Hague
recalc is a library for reasoning functionally about side effects. Its core ideas are: use events to pass control and a global store that is updated through events to pass data.
Current status of the project
The current version of recalc, v5.1.2, is considered to be stable and complete. Suggestions and patches are welcome. Besides bug fixes, there are no future changes planned.
recalc is part of the ustack, a set of libraries to build web applications which aims to be fully understandable by those who use it.
Installation
The dependencies of recalc are two:
recalc is written in Javascript. You can use it in the browser by sourcing the dependencies and the main file:
<script src="dale.js"></script>
<script src="teishi.js"></script>
<script src="recalc.js"></script>
Or you can use these links to the latest version - courtesy of jsDelivr.
<script src="https://cdn.jsdelivr.net/gh/fpereiro/dale@3199cebc19ec639abf242fd8788481b65c7dc3a3/dale.js"></script>
<script src="https://cdn.jsdelivr.net/gh/fpereiro/teishi@31a9cf552dbaee79fb1c2b7d12c6fad20f987983/teishi.js"></script>
<script src="https://cdn.jsdelivr.net/gh/fpereiro/recalc@38b1fc1c5397b27d9162147fddad103a43e9b826/recalc.js"></script>
And you also can use it in node.js. To install: npm install recalc
recalc should work in any version of node.js (tested in v0.8.0 and above). Browser compatibility has been tested in the following browsers:
- Google Chrome 15 and above.
- Mozilla Firefox 3 and above.
- Safari 4 and above.
- Internet Explorer 6 and above.
- Microsoft Edge 14 and above.
- Opera 10.6 and above.
- Yandex 14.12 and above.
The author wishes to thank Browserstack for providing tools to test cross-browser compatibility.
Philosophy
Recalc is a library for working with side effects in a functional way. Functional programming traditionally tries to marginalize side effects as much as possible, but I feel this is dishonest, because the side effects are still there and still needed. And as any form of denial, it ends up generating more trouble.
Programming in a functional style entails making programs out of functions. These functions call each other, in predetermined sequences, passing data between them exclusively in the form of arguments (inputs) and return values (outputs). Control and data are passed simultaneously: when a function returns, it both returns a value and also control to the function which called it in the first place. When all functions return, the program terminates.
These limitations (data & control being passed simultaneously, data being passed only as arguments and return values, and arguments not being modified by the function which receives them) are very empowering, because they make it much easier to reason about data and control flow, while still allowing for powerful programs.
Before I continue, let me say that I consider functional programming to be the most solid way of programming, for both small and large applications. Not only I consider it much better than imperative programming - I also think it is superior to object oriented programming. Functional is the paradigm I choose every time for all my projects.
However, there are certain circumstances where a pure functional approach won't work. Interestingly, virtually all the objections to functional programming which I consider valid emerge in these circumstances. It is also in these circumstances that many employ imperative or object oriented techniques.
In a decently pure functional program, there's only one side effect: when someone actually starts the program passing it some input. This both provides control and data to the main function of the program, which then calls other functions. When the whole thing is done, the main function dutifully returns its result and exits. In most cases, though, it is allowed for that function to commit another side effect to do something useful with its output, like writing that data to a file. So, besides the initialization and occasionally the exit, the rest of the program is purely functional.
The shape of this program, however, is pretty much identical to a traditional batch-and-queue program. The only interactions (or side effects, since they amount to the same thing) with the outside world are located only at the beginning and at the end of the process.
But an interactive program, like a game or an application, has to process interactions from the user and the server while it is running. The question is, how do we handle these interactions/side effects in a functional manner?
This library is an attempt to answer that question.
If we can't get rid of side effects, we can instead embrace them without forgetting our functional roots. We can consider that each side effect is an event. When a side effect comes in, we make our program call an event. And here's the kicker: there might be one or more responders for a given event. Those responders are merely functions that are executed as a consequence of a given event being called. In this way, you can work on the problem of passing control around when side effects come into the picture.
The other question is: how do we pass data around? Since there might be multiple responders to an event, operating sequentially or parallely, it would be messy to depend on return values. Instead, we can create a central object where we can store all the state of the application. A global object shared by all functions (I hope you're cringing now), which we call the store.
But wait! If these events and their handlers modify this global object, wouldn't be this merely be imperative programming? It would, except for one catch: data changes on the global state can also be expressed as events, to which functions can respond/react. So you don't just increment a variable. You increment it through an event that notifies everyone concerned. Events allow us to create first-class side effects, so that we can define them, pass them around and reason about them in a clear way.
From a functional perspective, this is tantamount to heresy. We're talking here about systematizing side effects, using a global state to pass data and ignoring the return values from event handlers. However, this is exactly how I think that functional programming should be extended to deal with programs which continually interact with the outside world. In other words, this is an attempt to find the general case of functional programming.
Programs as communication
The deep pattern behind recalc (and event systems in general) is communication. More precisely, the call and response pattern, which takes us to the origins of information.
By expressing computation as communication, some parts of the program perform calls and other parts of the program respond to those calls.
The two nouns with which we can structure this paradigm is: event and responder. The two corresponding verbs are call and match: events are called (almost always by responders), responders are matched by events. This constructive circularity can represent programs in an understandable and scalable way.
Usage
To use recalc, you first need to create a recalc object.
Let's assume that the recalc library has been loaded at the variable R
(this should happen automatically in the browser - in node.js, you would need to write var R = require ('recalc')
). To initialize a recalc object you would do this:
var r = R ();
r
will be now our recalc object. The reason we use a constructor (instead of directly making R
a recalc object) is that you may need to have multiple recalc objects at the same time; by executing the constructor more than once, you can get as many recalc objects as needed.
Every recalc object has six elements that you need to understand in order to use the library. The first three are objects, the other three are functions:
r.responders
r.store
r.log
r.call
r.respond
r.forget
r.responders
The responders
object is an object that contains all the responders. Whenever an event is called, recalc iterates all the elements within responders
, and executes those that match the event being called.
To add and remove responders, rather than directly modifying the array, I recommend you use r.respond
and r.forget
(we'll see these functions in a moment).
r.store
Our beloved global state. It is initailized as an empty object. If you want to initialize it to an array, you can do so by initializing r
like this:
var r = R ([]);
In this way, you can also initialize store
to contain some data, right off the bat:
var r = R ({some: 'important', data: true});
By the way, that's the only argument you can pass to R
(the constructor).
An important thing to notice is that the responders are stored outside of the global store.
r.log
An array that contains all the events called and all responders matched. While not essential for recalc's functioning, it is of great help for writing and debugging applications based on recalc. Each of the elements of this array are of the form {t: TIMESTAMP, id: STRING, from: STRING|UNDEFINED, verb: ..., path: ..., args: UNDEFINED|ARRAY}
. We'll see more of r.log
in the last section of the readme.
r.call
So far, responders
, store
and log
are simple objects. Where's the action? Enter r.call
. This function calls events.
The standard practice is to say that events are fired or triggered. We use the term call instead, in order to emphasize two things: 1) the complementary nature of calling and responding; 2) the fact that events represent communication.
Every event has two properties:
- A
verb
, which is a string. For example:'get'
,'set'
or'someverb'
. - A
path
, which can be either a string, an integer, or an array with zero or more strings or integers. For example,'hello'
,1
, or['hello', '1']
. If you pass a single string or integer, it will be interpreted as an array containing that element (for example,'hello'
is considered to be['hello']
and0
is considered to be[0]
). - Optional extra arguments of any type (we will refer to them as
args
later). These arguments will be passed to matching responders.
The combination of a verb plus a path seems versatile enough to serve as a general purpose way of building an event system. It works well for REST and it will probably work well for us too.
Both verb
and path
are required. If you don't pass them or pass invalid ones, r.call
will return false
. If its inputs are valid, the event will be called and the function will return a string with the id of the event just called (aren't side effects fun?).
For example, to call an event with verb 'fire'
and a path ['hello', 1]
, you can write:
r.call ('fire', ['hello', 1]);
You can pass unlimited additional arguments when you call an event. These arguments will be received by any responders that are responding to that particular event. For example, if you want to pass 'foo'
and 'bar'
, you can write:
r.call ('fire', ['hello', 1], 'foo', 'bar');
You can also use wildcards for both verbs and paths. For example:
r.call ('*', '*');
r.call ('fire', '*');
r.call ('fire', ['hello', '*']);
Wildcards are further explored in the next section - to really understand them, we need to understand how responders are matched against a new event.
r.respond
This function is the one in charge of placing responders into r.responders
. Every time it is invoked with valid arguments, it will add a responder. It takes the following arguments:
verb
, which can be a string or a regex.path
, which can be a string, an integer, a regex, or an array containing those types of elements.options
, an optional object with additional options.rfun
, the function that will be executed when the responder is matched.rfun
is short forresponder function
.
As with r.call
, if you pass invalid parameters, this function will return false
and print an error message. Otherwise it will return the id
of the responder.
It is important to notice that while call and respond are complementary operations, r.call
and r.respond
are not exactly complementary. r.call
calls a single event, whereas r.respond
creates a responder that can be matched multiple times over the course of a program. In other words, an invocation to r.call
represents a single execution (with multiple possible ramifications) whereas r.respond
represents multiple possible executions over the course of the program.
Let's see an example: to match the last event we showed on the last section, we could write a responder like this:
r.respond ('fire', ['hello', 1], function () {
// Your logic here.
});
Remember that we said earlier that the responders
object is an object that contains all the responders. Whenever an event is called, recalc iterates all the elements within responders
, and executes those that match the event being called. The most important question regarding responders is: when does a responder match an event? And the answer is: when both the verb and path of the called event match those of the responder.
In the case of the verb, matching is straightforward: the verb of the path must be identical to that of the responder.
In the case of the path, matching is also straightforward (as of recalc v5): the paths of both event and responder must have the same length and each of their elements must match. For example:
r.respond ('fire', 'hello', function () {...});
The responder above will match the following event:
r.call ('fire', 'hello');
But it will not match the following events:
r.call ('fire', ['hello', 'there']);
r.call ('fire', 'nothello');
r.call ('fire', []);
Historical notes:
- In recalc v3 and below, the responder path was the "measure" of comparison, and the event path was compared against it. This had the consequence that a responder with a path longer than an event path would not be matched, even if all its elements matched those of the path.
- In recalc v4, event and responder path were compared but if any of them had extra items, those were ignored. While better than v3 logic, for most cases it is best to leave non-trivial path comparison logic to the
match
functions in responders (see below).
If the path of the responder is a regex (or given element of a path is a regex), said regex will be used to match against the corresponding member of the event's path. For example, this responder:
r.respond ('hello', /foo|bar/, function () {...});
Will be matched by the first and third events below, but not the second.
r.call ('hello', 'foo');
r.call ('hello', 'bach');
r.call ('hello', 'bar');
responder verbs can also be regexes. For example, this responder:
r.respond (/foo|bar/, 'bach', function () {...});
Will match the following two events.
r.call ('foo', 'bach');
r.call ('bar', 'bach');
Let's see now which options
can be passed to a responder. options
All of them are optional:
id
: a string or integer that will uniquely identify a responder. If you don't pass one,r.respond
will generate one for you. Thisid
will be used as the key where the responder is bound - for example, ifoptions.id === 'hello'
, the responder will be stored atr.responders.hello
. If you pass anid
that's already being used by another responder, an error will be printed andr.respond
will returnfalse
. Ifid
is a string, it cannot be anR
followed by digits (or anE
followed by digits), since those ids are used by recalc when creating responders (without ids) and event ids (in general).parent
: a string or integer that will represent theid
of this responder. By default this isundefined
. The purpose of this will be explained below when we explainr.forget
.burn
: a boolean value. By default this isundefined
. When you set it totrue
, the responder will auto-destroy after being matched a single time. This allows you to create one-off events that later disappear, hence allowing you to keep clean the event space.match
: a function that is used to determine whether the responder matches a given event. This function works as a bypass to the standard matching logic defined inr.match
. This function receives two arguments: 1) an object of the form{verb: ..., path: ..., args: [...]}
which correspond to those of the event being called. If the event was called with noargs
(extra arguments),args
will be an empty array. 2) theresponder
itself. This function must returntrue
to indicate that the given event matches the responder; otherwise, the responder won't be matched for that particular event.priority
: an integer value. By default this isundefined
(which will be considered as a priority of0
). The higher the value, the sooner this responder will be matched, compared to other responders that also were matched. You can also use negative values.
This last property reminds us that it is perfectly normal to have more than one responder matching a certain event. priority
simply lets us make certain responders to be matched ahead of others. This allows for sophisticated (yet necessary) constructs like view redrawing, where some responders have to be called ahead of others to prevent unnecessary operations. As of recalc >= v4.1.0, responders of equal priority are run first by the order in they were created - older responders are matched before new ones (previous versions matched responders of equal priority in arbitrary order). To keep track of which responders were created first, an index
parameter is added to each responder
- the lower the index
, the earlier the responder was created.
It is also perfectly possible to have zero responders matching a certain event.
Here's an example of how to invoke r.respond
with options.
r.respond ('fire', 'test', {id: 'matchEverything', parent: 'someOtherresponderId', priority: 3, burn: false}, function () {...});
You can also place the verb
and the path
inside the options object. In this case, options
becomes the first argument passed to the function, and is now mandatory (since r.respond
must receive a verb
and a path
always).
r.respond ({verb: 'fire', path: 'test', id: 'matchEverything', parent: 'someOtherresponderId', priority: 3, burn: false}, function () {...});
r.respond
advanced matching: wildcards & empty paths
Within event and responder paths, you can also use wildcards. For example, this responder will be matched by any event that has a path that's exactly one element long:
r.respond ('*', '*', function () {...});
You can also use wildcards for single elements in an array path. For example, this responder:
r.respond ('fire', ['hello', '*', 'handsome'], function () {...});
Will be matched by the following events:
r.call ('fire', ['hello', 'there', 'handsome']);
r.call ('fire', ['hello', 'out there', 'handsome']);
r.call ('*', ['hello', '*', '*']);
A path can also be an empty array. Conceptually, an empty path represents the root path. For example, this responder:
r.respond ('fire', []);
will only match the following event:
r.call ('fire', []);
But not the following events:
r.call ('fire', '*');
r.call ('fire', ['another', 'path']);
To make a responder match any event, you can pass a match
attribute like the following:
r.respond ('*', [], function () {return true});
To make a responder match all event with a matching verb, you can pass a match
attribute like the following:
r.respond ('someverb', [], function (ev, responder) {return r.compare (ev.verb, responder.verb)});
In both examples above, the path
passed to r.respond
makes no difference.
r.forget
This function allows us to remove responders. Its first argument is id
, which should be a string or integer. This id
is the one of the responder we are removing.
If you call r.forget ('foo')
, this will also remove all the responders that have 'foo'
as its parent, plus all their children. In effect, the parent
property of the responder allows for tree-like deletion of responders.
If you call r.forget ('foo')
and there's no responder named foo
, this function will return false
and print an error.
If you pass a function as the second argument to r.forget
, this function will be executed for every responder deleted (that is, the responder with the id
specified, plus all its children). This onforget
function will receive as first argument the responder that is being deleted.
For example, r.forget ('foo', function (r) {console.log (r.id)})
will print foo
, as well as the ids of any responders that have foo
as parent.
If you pass a function as the second argument to r.forget
, the responder being deleted will be removed from r.responders
before this function is executed.
r.count
For the sake of completeness, we mention a seventh element, r.count
, which is a mere counter variable to keep track of how many events and responders we added so far. This variable serves for generating incrementing ids for events and responders. The index
variable is added by r.respond
to each responder references r.count
, solely for the purpose of disambiguating the matching order of responders that have the same priority (older responders are matched first).
responder functions
I lied - you also need to understand how responder functions work in order to use recalc. But this is the very last thing.
A responder function always receives an execution context
(or merely context) as its first argument. The context is an object with six elements:
verb
, the verb of the event that matched the responder.path
, the path of the event that matched the responder.args
, an array with extra arguments passed to the event. If no extra arguments were passed, this element will be undefined.from
, the id of the event that matched this responder (which is the same value returned by the correspondingr.call
invocation).cb
, a callback function which you only need to use if your responder function is asynchronous.responder
, the matched responder.
As a convention, I name context
as x
. Let's see an example:
r.respond ('fire', '*', function (x, extraArg1, extraArg2) {
// Do something...
});
If we call the following event:
r.call ('fire', 'hello');
Then the responder above will receive an x
of the form {verb: 'fire', path: ['hello'], cb: ..., responder: {...}, from: ...}
and both extraArg1
and extraArg2
will be undefined
.
To set extraArg1
and extraArg2
to 'foo'
and 'bar'
respectively, you could call the following event:
r.call ('fire', 'hello', 'foo', 'bar');
This means that any parameters passed to r.call
besides verb
and path
will be passed to the responder function.
In this case, the x
will be {verb: 'fire', path: ['hello'], args: ['foo', 'bar'], cb: ..., responder: {...}, from: ...}
.
Having covered the arguments, we now have to see what you to have in mind when writing responder functions. There's two issues: control and data.
Regarding control, if your responder function is synchronous, there's nothing to worry about: once your function finishes executing, the responder functions of any other responders will be executed, one at a time. However, if your function is asynchronous, you need to signal this to recalc, so that the order in which responders are matched will be preserved.
Let's say that you have a responder function that executes an asynchronous operation. The proper way to write it is like this:
r.respond ('fire', 'hello', function (x) {
asyncOperation ('foo', 'bar', function () {
x.cb ();
});
return x.cb;
});
The above async responder function does two things that every async responder function should do:
- Call
x.cb
from within the asynchronous callback, to resume execution of further matching responders. - Return
x.cb
, to notify recalc that this is an async function and it should not keep on executing other responders synchronously.
Interestingly enough, if you don't want to resume the execution of further responders, you can skip the call to x.cb
from within the asynchronous callback. This may be handy in case of an error.
Note that if two responders are matched for a single call, and the responder with the highest priority is async, the second responder will be only called after the first responder finished its async operation.
Regarding the issue of calling events from within a responder (something which is both possible and necessary), it can be done freely as long as the events match responders with synchronous responder functions. If, however, you call an event that matches one or more asynchronous responder functions, you should do that as the last thing within the body of your responder function, in order to preserve the sequence. Not only that, you should also return x.cb
in order to signal that this operation was async, and hence your responder function is async as well.
Regarding data: unless your responder function returns x.cb
(which signals that it is async), any other return values will be discarded. If you want to pass data, you should place it in r.store
, through a function that calls an event.
Because recalc wants to retain generality, we haven't bundled any functions for calling updates on r.store
using the event system. This is done on gotoŠ², which is built on top of recalc. However, consider the following example, where r.store
is an array:
r.respond ('setElement', '*', function (x, value) {
r.store [x.path] = element;
});
r.call ('setElement', 2, 'something');
The two functions above will set the third element of r.store
to the value 'something'
. This function can be generalized to arrays and objects, and also to support deletions.
Tracking event & responder chains
Whenever an event is called with r.call
, or whenever a responder is matched by an event call, an entry will be added to r.log
with the following fields: {t: TIMESTAMP, id: STRING, from: STRING|UNDEFINED, verb: ..., path: ..., args: UNDEFINED|ARRAY}
. id
corresponds to the value returned by a successful invocation to r.call
or r.respond
(the id
of either the event or the responder).
If you are inside a rfun
and you want to call further events with r.call
, you can pass the context (x
) argument as the first argument to r.call
. This will allow you to create a trackable chain of events.
This is quite hard to grasp conceptually, so let's see an example:
r.respond ('hello', 'there', {id: 'vamo'}, function (x) {
r.call (x, 'goodbye', 'goodbye');
});
r.call ('hello', 'there');
We first define a rfun
. Whenever it gets matched, we call r.call
and we pass the context.
After that, we call an event. Let's say this event will return a value of id1
(just for exemplary purposes; actual ids generated by recalc are longer). When the rfun
we defined above is matched, x.from
will have a value of id1
(which is the id of the event that matched that rfun
).
The invocation to r.call
from within the rfun
will return another id (let's call it id2
). The interesting part will happen in r.log
, which will look like this:
[
{id: 'id1', from: undefined, verb: 'hello', path: ['there'], t: ..., args: undefined},
{id: 'vamo', from: 'id1', verb: 'hello', path: ['there'], t: ..., args: undefined},
{id: 'id2', from: 'vamo/id1', verb: 'goodbye', path: ['goodbye'], t: ..., args: undefined},
]
As you can see, the log for id2
will mark that this event came from vamo/id1
, which denotes that it was called from the responder with id
vamo
, which in turn was matched by event id1
. When an event is called by a responder which in turn was matched by a previous event, recalc puts the id
of the responder followed by a slash and the id
of the previous event. This is necessary when a responder is matched multiple times - by specifying the id
of the event that matched the responder, the chain of execution can be disambiguated.
Any rfuns
matched by id2
will receive id2
as its x.from
, and if you pass the context to further event invocations, you'll have longer chains of dependencies. In this way, you will have the raw data to track complex (and often asynchronous) events, which can be useful in many applications.
Two more things to have in mind: if you don't pass x
to an invocation to r.call
within a rfun
, recalc has no way of knowing where that event came from and then the chain will be broken. The second one is that initial invocations of r.call
(that happen on their own, without being the result of previous events) will naturally receive no context, since there's no context yet for them.
Internally, recalc uses the function B.addLog
to add elements to B.log
. The function receives the log object that will be pushed to B.log
and by default pushes it to B.log
. This function can be overridden for different purposes:
- If you want to turn off logging, you can set
r.addLog
to an empty function:r.addLog = function () {};
. - You can override
r.addLog
to overwrite sensitive information of different entries, or ignore certain log entries altogether. - You can limit the size of
r.log
if it gets too large, either by not pushing further logs or by eliminating the first element ofB.log
before pushing a new one.
r.prod
and r.error
In addition to the six main elements of a recalc instance, there's two more that you can optionally use.
The first one is r.prod
. If you set this flag to true
, all the recalc functions will stop performing validations. This can be useful to speed up a thoroughly debugged application - you can roughly expect to save execution time by ~20% when invoking r.call
and by ~90% when invoking r.respond
. Please be aware that if you turn on r.prod
and there's an error in your code, you'll experience either an exception or unexpected behavior, so be sure to turn this on only when you are very confident of your code.
The second one is r.error
. This is a function that is executed whenever one of recalc's functions finds a validation error - if any recalc function receives invalid inputs, it will invoke r.error
and pass to it one or more arguments describing the error. r.error
is by default mapped to teishi.clog
, which prints output to the console and returns false
.
If you want to override r.error
, make sure that the function returns false
, otherwise It defaults to printing errors to the console, but you can override it with another function for custom error reporting.
Turning off a responder
If you set a responders disabled
property to true
(i.e.: r.responders.SOMEID.disabled = true
), said responder won't be matched by any events until the property is set to false
or eliminated. This allows for temporarily disabling a responder without having to store it in a variable, remove it and later add it again.
Internals
There's six other functions that support the usage functions. If you override them, you can change the innards of recalc. These are:
r.isPath
, a helper function to determine whether something is a validpath
.r.compare
, a helper function for comparing a responder item (either the verb or one of the elements of the path) with a corresponding event item; this function is used byr.match
.r.addLog
, a helper function that pushes a log entry tor.log
and can be overriden to customize logging.r.mill
, the function that gets executed byr.call
when an event is called - it represents the core engine of the library.r.match
, the function in charge of matching an event with the relevant responders.r.sort
, the function that sorts the order of the matching responders that will be executed in response to a certain event.
Source code
The complete source code is contained in recalc.js
. It is about 200 lines long.
Below is the annotated source.
/*
recalc - v5.1.2
Written by Federico Pereiro (fpereiro@gmail.com) and released into the public domain.
Please refer to readme.md to read the annotated source.
*/
Setup
We wrap the entire file in a self-executing anonymous function. This practice is commonly named the javascript module pattern. The purpose of it is to wrap our code in a closure and hence avoid making the local variables we define here to be available outside of this module. A cursory test indicates that local variables exceed the scope of a script in the browser, but not in node.js. This means that this pattern is useful only on the browser.
(function () {
Since this file must run both in the browser and in node.js, we define a variable isNode
to check where we are. The exports
object only exists in node.js.
var isNode = typeof exports === 'object';
We require dale and teishi. Note that, in the browser, dale
and teishi
will be loaded as global variables.
var dale = isNode ? require ('dale') : window.dale;
var teishi = isNode ? require ('teishi') : window.teishi;
We create an alias to teishi.type
, the function for finding out the type of an element. We do the same for teishi.clog
, a function for printing logs that also returns false
.
var type = teishi.type, log = teishi.clog;
Constructor
We define a main
function that will return a recalc object. A function returning an object which contains the functionality of a library is commonly known as constructor. Recalc is built with a constructor pattern because it is reasonable to assume that you might want to use more than one recalc object at the same time; in this case, the constructor will be invoked once per each object that the user desires to create.
main
takes a single argument, a store
.
var main = function (store) {
The store
can be undefined
, an array or an object. By passing a store
to main
, the user can already initialize the store to have certain values.
Note we pass true
as the fourth argument to teishi.stop
. We will do this for every invocation of teishi.stop
and teishi.v
, to tell teishi not to validate our validation rules. This will yield a (very small) performance improvement.
if (teishi.stop ([
['store', store, ['array', 'object', 'undefined'], 'oneOf']
], undefined, true)) return;
We create r
, a local object which will be the sole thing returned by the constructor. We'll put everything inside of r
.
var r = {
We set r.responders
to be an empty object; as for r.store
, we set it to the store
passed to main
, else we initialize it to an empty object. Notice that the responders
are going to be stored outside the store
.
responders: {},
store: store || {},
We set r.log
to an empty array.
log: [],
We set r.error
to clog
(teishi.clog
). By using r.error
below to report errors, we make it possible to report errors in a different way other than printing them to the console.
error: clog,
Finally we set r.count
to an object with two keys e
and r
, each initialized to 1; we will use this variable to generate ids for events in r.call
and in r.respond
to register the order in which responders were added and optionally to generate responder ids.
count: {e: 1, r: 1}
}
The three usage functions
We now define the first of the three usage functions, r.call
, which serves the purpose of firing an event.
r.call = function () {
r.call
is a variadic function (this means it takes a variable number of arguments). While it must always receive a verb
and a path
, it can optionally receive a context
(named x
, here and henceforth). Since the verb
cannot be an object, we consider x
to be present if the first argument is an object. We then consider the following arguments to be the verb
and the path
.
var x = type (arguments [0]) === 'object' ? arguments [0] : undefined;
var verb = arguments [x ? 1 : 0];
var path = arguments [x ? 2 : 1];
A valid path can only be an array. However, it is quite convenient to write paths with a single element (['foo']
) as the element itself ('foo'
); for this reason, r.call
automatically wraps any simple path (ie: neither an array nor an object) into an array, using teishi.simple
to detect whether this is the case.
if (teishi.simple (path)) path = [path];
If r.prod
is falsy, we validate the arguments passed to r.call
.
if (! r.prod && teishi.stop ('r.call', [
If x
is not undefined
, it must be an object (remember that when detecting whether x
was passed, we only considered that to happen only if the first argument was an object).
['context', x, ['object', 'undefined'], 'oneOf'],
If x
is present, we check that x.from
should be either a string or undefined.
[x !== undefined, [function () {
return ['x.from', x.from, ['string', 'undefined'], 'oneOf'];
}]],
This concludes our validation of x
.
We check that verb
is a string.
['verb', verb, 'string']
If any part of the validation fails, we return false
. Note we pass r.error
as an apres argument so that this function will also receive the validation error, if any.
], function (error) {
x ? r.error (x, 'r.call', error) : r.error ('r.call', error);
}, true)) return false;
If r.prod
is falsy, we check that path
is valid through a helper function r.isPath
that will return true
or false
. While we'll see the implementation of r.isPath
later, this function makes sure that path
is an array composed of strings and integers. In case of error, we print it through r.error
and return false
.
if (! r.prod && ! r.isPath (path)) return x ? r.error (x, 'r.call', 'Invalid path. Arguments:', {verb: verb, path: path}) : r.error ('r.call', 'Invalid path. Arguments:', {verb: verb, path: path});
We define two local variables. First, oargs
, which is a mere reference to arguments
. Second, we create args
, which will be either an array with all the "extra" arguments received by r.call
(that is, arguments beyond x
, verb
and path
). If no extra arguments are passed, this variable will be undefined.
args
is done by iterating the arguments
pseudo-array and using oargs
as a reference to it.
var oargs = arguments;
var args = arguments.length === (x ? 3 : 2) ? undefined : dale.go (dale.times (arguments.length - (x ? 3 : 2), x ? 3 : 2), function (k) {
return oargs [k];
});
Notice that if x
was passed, extra arguments will be those starting with index 3 (fourth element and above), while if x
wasn't passed, extra arguments will be those starting with index 2 (third element and above).
We define a local variable from
and set it to either x.responder.id/x.from
(if x.responder
is present) or x.from
, only if x
is present. If present, this will represent the id of a previous responder (or a previous event) that is calling the current event. If x.responder
is present, x.responder.id
will be defined, and so will be x.from
(since a responder can only be matched by an event, and events always have ids
).
var from = x ? (x.responder ? (x.responder.id + '/' + x.from) : x.from) : undefined;
We now set x
to a brand new object with the following fields: from
, with a freshly generated id prepended by an E
(using and incrementing r.count.e
); the verb
and path
received as arguments; and args
, the array (or undefined) variable we just created above.
Notice that if x
was passed to this invocation of r.call
, we have already overwritten it and only kept a reference to the original x.from
- no other field of the original context is used.
x = {from: 'E' + r.count.e++, verb: verb, path: path, args: args};
We invoke a helper function, r.addLog
, to add a log entry to r.log
. We will cover this function later. This function receives the from
field from the old context (if present, otherwise it will be undefined), the id of the current event being called, the verb
and path
received, and the args
variable containing further arguments. Note we also pass a timestamp in the t
field. Also note that we copy args
in case they are modified later, so we have a snapshot of them at the moment we log this event.
r.addLog ({t: teishi.time (), from: from, id: x.from, verb: verb, path: path, args: teishi.copy (args)});
We call an internal function, r.mill
, passing it an array with all the arguments. If there are no extra arguments, we merely pass x
wrapped in an array; otherwise, we pass an array with x
plus all the extra arguments. Note that x
already contains verb
and path
.
r.mill (args === undefined ? [x] : [x].concat (args));
We return x.from
, which contains an id to identify the current event invocation. This concludes the function.
return x.from;
}
We now define r.respond
, a function for creating responders
, which are functions that are executed when a matching event is called.
r.respond = function () {
We set two local variables: options
(as of yet uninitialized) and rfun
(set to the last argument passed to r.respond
. options
will be an object with options passed to the responder, while rfun
will be the responder function.
var options, rfun = arguments [arguments.length - 1];
r.respond
is a variadic function that must receive a minimum of two arguments. If it receives less than two arguments, we will print an error and return false
.
if (arguments.length < 2) return r.error ('r.respond', 'Too few arguments passed to r.respond');
If we receive two arguments only, we'll consider options
to be the first one. Since we already set rfun
to the last argument received, there's nothing else to do in this case.
if (arguments.length === 2) options = arguments [0];
If there's more than two arguments, the function expects verb
and path
to be the first two arguments. If there's four arguments in total, the function expects options
to be the third argument. In either case, rfun
will be considered to be the last argument. Notice that we place both the verb
and path
inside options
.
else {
options = arguments.length === 3 ? {} : arguments [2];
options.verb = arguments [0];
options.path = arguments [1];
}
In the same way as r.call
, r.respond
automatically wraps any simple path (ie: neither an array nor an object) into an array, using teishi.simple
to detect whether this is the case.
if (teishi.simple (options.path)) options.path = [options.path];
If r.prod
is falsy, we validate the arguments.
if (! r.prod && teishi.stop ('r.respond', [
options
must be an object and only certain keys are allowed in it.
['options', options, 'object'],
['keys of options', dale.keys (options), ['verb', 'path', 'id', 'parent', 'priority', 'burn', 'match'], 'eachOf', teishi.test.equal],
options.verb
can be either a string or a regex.
function () {return [
['options.verb', options.verb, ['string', 'regex'], 'oneOf'],
If defined, both options.id
and options.parent
must be strings or integers.
['options.id', options.id, ['string', 'integer', 'undefined'], 'oneOf'],
['options.parent', options.parent, ['string', 'integer', 'undefined'], 'oneOf'],
If defined, options.priority
must be an integer
, as options.burn
must be a boolean.
['options.priority', options.priority, ['undefined', 'integer'], 'oneOf'],
['options.burn', options.burn, ['undefined', 'boolean'], 'oneOf'],
If defined, options.match
must be a function.
['options.match', options.match, ['undefined', 'function'], 'oneOf']
]},
rfun
must be a function.
['responder function', rfun, 'function']
If any part of the validation fails, we return false
. Note we pass r.error
as an apres argument so that this function will also receive the validation error, if any.
], function (error) {
r.error ('r.respond', error);
}, true)) return false;
If r.prod
is falsy, we validate path
with r.isPath
, which we'll define later. In this case, we consider a path
as valid if it's an array composed of strings, integers and regexes (which is the reason for passing true
as the second argument to r.isPath
). If there's an error, we print it through r.error
and return false
.
if (! r.prod && ! r.isPath (options.path, true)) return r.error ('r.respond', 'Invalid path. Options:', options);
If options.id
is not undefined
, we first coerce it into a string (in case it is an integer). Second, if r.prod
is falsy, we check that 1) the id has a shape different to Rd...
(where d
represents a digit) or Ed...
since those id forms are used by recalc; and 2) that there's no responder within r.responders
with that id. If it does, we print an error and return false
.
if (options.id !== undefined) {
options.id += '';
if (! r.prod && options.id.match (/^[R|E]\d+$/)) return r.error ('r.respond', 'If you pass an id for a responder, it cannot be an `R` or an `E` followed by digits, since that is the form that recalc gives to auto-generated ids.');
if (! r.prod && r.responders [options.id]) return r.error ('r.respond', 'A responder with id', options.id, 'already exists.');
}
If options.id
is undefined
, we generate an id of the form R
followed by the current value of r.count.r
(which is a number).
else options.id = 'R' + r.count.r;
We place rfun
within the options
object.
options.rfun = rfun;
We set options.index
to the current value of r.count.r
, and increment r.count.r
.
options.index = r.count.r++;
By this point, options
contains all the information relevant for the responder - it is effectively the responder object. We now set it within r.responders
.
r.responders [options.id] = options;
We return the id
of the responder and close the function.
return options.id;
}
We now define the third and last usage function of recalc, r.forget
, which has the purpose of removing responders from r.responders
.
This function takes two arguments, id
and fun
. The first one is the id
of the responder that must be removed. fun
is an optional function that will be executed when the responder is deleted.
r.forget = function (id, fun) {
If present, fun
must be a function; otherwise we print an error and return false
. Note we skip the check if r.prod
is falsy.
if (! r.prod && fun !== undefined && type (fun) !== 'function') return r.error ('r.forget', 'Second argument to r.forget must be a function or undefined. Id is:', id);
If r.prod
is falsy and the responder does not exist, we print an error and return false
.
if (! r.prod && ! r.responders [id]) return r.error ('r.forget', 'responder', id, 'does not exist.');
We store the responder in a local variable responder
. We then remove it from r.responders
.
var responder = r.responders [id];
delete r.responders [id];
If fun
is present, we invoke it passing responder
as its first argument. This is why we stored responder
locally, so that we could pass it to this function after the responder was removed from r.responders
.
if (fun) fun (responder);
We iterate all the other responders; if any of them has id
as its parent, we recursively call r.forget
on it, taking care to also pass fun
. This allows for tree-like deletion of responders. Notice that if other responders have id
as a parent, fun
will be executed multiple times.
dale.go (r.responders, function (v, k) {
if (v.parent === id) r.forget (k, fun);
});
There's nothing else to do, so we close the function.
}
Internals
We now define r.isPath
, a helper function that determines whether a path
is valid. Besides path
it takes an optional regex
flag.
This function is used by the two main functions, once by r.call
and once by r.respond
.
r.isPath = function (path, fun, regex) {
path
can be either an array, an integer or a string. If it's an array, it must be comprised of integers and strings. If the regex
flag is passed, path
can also be a regex (and if it's an array, it can also include regexes).
The regex
flag is used by r.respond
, because responder paths can be (or include) regexes, unlike event paths.
We return the result of invoking teishi.v
with these rules. We also pass an empty apres
argument to teishi.v
so that if there's an error, we don't print it.
return teishi.v ([
['path', path, ['array', 'integer', 'string'].concat (regex ? 'regex' : []), 'oneOf'],
['path', path, ['integer', 'string'].concat (regex ? 'regex' : []), 'eachOf']
], function () {}, true);
}
We define the second helper function, r.compare
. It takes two arguments, a verb
or path
element belonging to an event and a verb
or path
element belonging to a responder. This function is used later by r.match
to compare verbs and paths.
r.compare = function (eventItem, responderItem) {
If either of the arguments is a string containing a star (wildcard), the function returns true
.
if (eventItem === '*' || responderItem === '*') return true;
If the responder element (its verb
or a part of its path
), we check whether the corresponding event element matches it. Notice we coerce the event element to a string in case it's an integer. Depending on whether there's a match or not, the function will return true
or false
.
if (type (responderItem) === 'regex') return (eventItem + '').match (responderItem) !== null;
If we're here, it means that both elements are either strings or integers. The only way there can be a match is if they're identical. We return the result of this comparison and close the function.
return eventItem === responderItem;
}
We define r.addLog
, the third and last helper function, which adds log entries to r.log
and is invoked only by r.call
and r.mill
. The function takes a single argument, the log object to be pushed.
r.addLog = function (log) {
r.log.push (log);
}
We now define r.mill
, the implementation function that represents the core engine of the library. This function is invoked by r.call
. The function takes a single argument, args
, which is an array with the context (x
) plus other optional arguments.
r.mill = function (args) {
We define an inner
function that will execute each of the responders matching the event in turn. matching
, its sole argument, must be an array containing responders.
var inner = function (matching) {
If matching
is empty, there's nothing else to do, so we return undefined
.
if (matching.length === 0) return;
We set up a cb
(callback) within the context of args; the purpose of this function is to be executed by asynchronous responder functions, so as to preserve the order in which the responder functions are executed, even when some of them are asynchronous and some of them are not. Notice that the callback simply executes inner
recursively.
args [0].cb = function () {
inner (matching);
}
We pick the first responder within matching
and remove it from the array.
var responder = matching.shift ();
We set the responder
of the context to the responder itself.
args [0].responder = responder;
If the responder does not exist, we ignore this responder and call inner
recursively. This check is useful if a certain responder function removes other responder functions that were matched by the same event.
if (! r.responders [responder.id]) return inner (matching);
We invoke r.addLog
to add a log entry to r.log
for the matched responder. As before, this function receives the from
field from the old context (if present, otherwise it will be undefined), the id of the responder being matched, the verb
and path
of the responder; if args
were passed to the responder, we pass them as well, otherwise we pass undefined
as the last argument to denote their absence. Note that we copy args
so that if they are modified later, we'll have a snapshot of them at the moment of logging.
r.addLog ({t: teishi.time (), from: args [0].from, id: responder.id, verb: responder.verb, path: responder.path, args: args.slice (1).length ? teishi.copy (args.slice (1)) : undefined});
If the responder has the burn
attribute, we invoke r.forget
to remove it.
if (responder.burn) r.forget (responder.id);
inner
does two more things. First, it invokes the responder function of the responder, passing args
to it. Second, if the rfun
did not return a function, then it invokes itself recursively.
Why does inner
avoid calling itself to process the next responder if rfun
returns a function? Because in case rfun
is asynchronous, it must signal that to recalc, and to do so it returns a function. However, the asynchronous rfun
is responsible for invoking inner
to execute any other responders; by making cb
available to the rfun
, we allow asynchronous functions to resume execution of other matching responders for the same event.
if (type (responder.rfun.apply (null, args)) !== 'function') inner (matching);
}
The last thing left to do is to call inner
with the matching responders. For this, we invoke r.match
with the verb
, path
, any extra arguments passed to the event invocation, and the list of responders. We then invoke r.sort
on the output of r.match
, so that the responders are placed in the right order.
inner (r.sort (r.match (args [0].verb, args [0].path, r.responders, args.slice (1))));
}
This concludes r.mill
. Let's now define r.match
, which is the function that takes a verb
, a path
, optional args
and a list of responders and returns only those responders that match an incoming event.
Instead of reading r.responders
directly, it is possible to use r.match
to scan a different set of responders - this is the reason for which responders
is an argument instead of internally being set to r.responders
.
r.match = function (verb, path, args, responders) {
We initialize a matching
array to store the responders that match the verb
and path
.
var matching = [];
We iterate the responders
.
dale.go (responders, function (responder) {
If the responder is disabled, we ignore it.
if (responder.disabled) return;
If the responder contains a match
function, we use that function to determine whether the responder matches the event. We invoke that function passing an object with verb
, path
and args
, plus the responder. Depending on whether that function returned true
or not, we push the responder
to matching
; in any case, we're done processing this particular responder.
if (responder.match) return responder.match ({verb: verb, path: path, args: args}, responder) === true ? matching.push (responder) : undefined;
We invoke r.compare
to compare the verb
of the responder with that of the event. If the comparison is not successful, the responder doesn't match.
if (! r.compare (verb, responder.verb)) return;
If we're here, the verbs match!
If the paths don't have equal length, the responder cannot be a match, so we return undefined
.
if (path.length !== responder.path.length) return;
If both the responder and the event have paths
of length 0, then the responder will be a match. We push responder
to the list of matching responders.
if (path.length === 0 && responder.path.length === 0) return matching.push (responder);
We compare both paths. If any of the comparisons returns false
, the loop will be interrupted.
if (dale.stop (path, false, function (v, k) {
We use r.compare
to compare the kth elements of both paths. r.compare
returns only true
or false
, and if it returns the latter it means that the comparison failed.
return r.compare (path [k], responder.path [k]);
If the last iteration of the loop returned true
, all iterated elements of both paths match with each other. If this is the case, we push the responder
to matching
. Notice that if any of the iterations returned false
, this will not happen.
})) matching.push (responder);
We close the iteration of the responders.
});
We return the array of matching responders and close the function.
return matching;
}
We define r.sort
, a function that takes a list of responders (outputted by r.match
) and sorts it by priority.
r.sort = function (matching) {
We apply sort
on matching
, using a custom sort function. Notice that we modify matching
instead of returning a new sorted array.
return matching.sort (function (a, b) {
We sort the responders based on their priority
value. If no priority
is defined, we'll consider the responder to have a priority
of 0. The higher the priority, the closer a responder will be to the beginning of the array.
If both priorities are the same, we disambiguate by giving priority to the responder with the lower index
. Since oldest responders have a lower index, given the same priority, the older responder will get matched first.
var priority = (b.priority || 0) - (a.priority || 0);
return priority !== 0 ? priority : a.index - b.index;
});
}
We are done defining recalc's methods. We return the r
object and close main
.
return r;
}
If we are in node, we set module.exports
to main. If we're in the browser, we create a global variable named R
and place main
there.
if (isNode) module.exports = main;
else window.R = main;
We close the module.
}) ();
License
recalc is written by Federico Pereiro (fpereiro@gmail.com) and released into the public domain.