Putting The Functional in Functional Reactive Programming

The mindful developer writes code that is as a stream, flowing yet still, strong yet yielding. Also, curried functions and stuff.
Reactive programming is a powerful way to manage the combinatorial growth in complexity of an application. Each event-mutation pair is independent from the rest. And modeling them in terms of event streams, instead of individual event handlers, helps us maintain that independence.
We can use functional programming constructs
to unify both collections and streams.
We can use the same functions to operate on both.
For example, we can use map
to apply
a function to elements in a stream or collection.
A functional approach also allows us to
introduce new operations without affecting
existing code.
But how do we accomplish this magic?
Example: Asset Pipeline
Our goal is to be able to use ordinary collection functions, like this:
go [
glob "**/*.jade", source
reject isMatch /(^|\/)_/
map context
tee compileJade
tee write target
]
This example is from Haiku9, which, together with Panda-9000 implements an asset pipeline for static-site generation based on Fairmont, our functional reactive programming framework.
What’s really going on here?
Redefining map
Let’s start our exploration with ordinary collection values, like arrays.
We can use JavaScript’s map
function to square all the elements of an array.
numbers = [1..10]
square = (x) -> x * x
assert (numers.map square)[9] == 100
To get to where we want to go,
we need an improved version of map
.
We want to be able to curry it, meaning we want to be able
to easily bind the function argument without the collection argument.
Why is currying important?
Look at the Haiku9 code again.
All the collection operations take function arguments, but that’s all.
We pass in the collection (or the event stream) later.
In order to curry a function, we need a standalone function,
not a method on an object.
We also need to take the function argument first,
so that we can curry that and pass in the collection later.
Fairmont provides an implementation of curry
for us,
so we can simply write our new map
function like this.
map = curry (f, c) -> c.map f
numbers = [1..10]
square = (x) -> x * x
Since currying map
just returns another function,
we are now free to employ another tool from our functional toolbox:
composition.
Getting To Flow
Let’s go back to our Haiku9 asset pipeline.
Each step in the pipeline is consists of curried functions.
The reject
function is a negated variant
of JavaScript’s Array::filter
function.
We use that to filter out pathnames with underscores.
We use map
to turn a pathname into
a Panda-9000 task context.
The tee
function is just like map
,
except it returns each argument unchanged,
regardless of what its function returns.
Here we curry it with a function to compile Jade code
and to write the result out to a file.
We can compose this entire pipeline into a single function.
(We’ll use Fairmont’s map
function from here on out.
We can curry it, just like the one we defined above.)
pipeline = compose (tee write target), (tee compileJade),
(map context), (reject isMatch /(^|\/)_/)
That’s a bit awkward, though.
The functions are in reverse order relative to their execution.
Fairmont provides another function, pipe
,
which allows us to express this a bit more naturally.
pipeline = pipe (reject isMatch /(^|\/)_/),
(map context), (tee compileJade), (tee write target)
This is still less than ideal, since we want to bind our function
to a collection (or event stream).
With pipe
, we end up writing it like this:
pipeline = ->
(pipe (reject isMatch /(^|\/)_/),
(map context), (tee compileJade),
(tee write target))(glob "**/*.jade", source)
This is still hard to read,
especially since the glob
function,
which kicks everything off,
is at the end.
Fairmont provides another composition operator for this exact reason.
The flow
function takes an iterator
(which abstracts a collection or event stream)
and a list of functions to compose.
pipeline = flow [
glob "**/*.jade", source
reject isMatch /(^|\/)_/
map context
tee compileJade
tee write target
]
Go
The only thing awkward about this is that flow
itself
returns an iterator. We can “run” the iterator easily enough
with any function that accepts iterators.
But it might be nice to just run it directly.
The final step in our transformation comes
thanks to another helper function in Fairmont,
go
, which does exactly that.
go [
glob "**/*.jade", source
reject isMatch /(^|\/)_/
map context
tee compileJade
tee write target
]
Extensibility
Let’s go back now to squaring numbers. Here is a simple “flow” that squares numbers from a given source.
squares = map square
Nothing too exciting there. Let’s add to this by doubling the number.
double = (n) -> 2 * n
f = (numbers) ->
go [
numbers
map square
map double
]
assert
Of course, we could have just written this as a single function.
f = (numbers) -> map numbers, (n) -> 2 * (square n)
and, in this case, that’s probably the right thing to do. But in more complicated scenarios, as with our asset pipeline, this style can be easier to write, read, and reason about.
We can also easily add new processing operations.
We can define any function that takes an iterator
(or reactor, which is just Fairmont-speak for an asynchronous iterator)
and use it the same way.
For example, we could define a sample
function
that samples a percentage of the values in the flow.
go [
numbers
sample 1/10
map square
map double
]
We can define sample
as a simple standalone function.
This is an advantage over most reactive programming libraries
(even some that call themselves functional reactive programming libraries)
that use method chaining instead of standalone functions.
For example, in Bacon.js
you must define an event stream and call methods on it.
If we want to add a new function, like sample
,
we’d have to add it to a Bacon.EventStream
object
or submit a patch to the Bacon.js project.
Whereas Fairmont, because it’s literally just a collection
of functions that you import into your code,
is more naturally extensible.
And That’s The F in FRP
We’ve seen how currying and composition lend themselves to building up sophisticated pipelines that can help us with practical tasks, like defining asset pipelines. This is a compelling example of how functional programming constructs can make our code more reusable and extensible. In combination with reactive programming (and JavaScript iterators) they give us a powerful and expressive way to write code, and even to think about and model designs.