FRP to me means building your app by transforming values over time, from the input to the current state to the display.
This implementation is based on a the graphics library and is heavily inspired by Elm
A full implementation of TodoFRP can be found online at Raynos/graphics example server
In building this TodoFRP app we have moved away from the MVC paradigm to a more reactive & declarative paradigm.
Our application is build of a few building blocks. An important start of the application is defining what the application state actually is.
// Application Model
var TodoModel = { id: "", title: "", completed: false, editing: false }
var TodosModel = { todos: [], route: "all" }
Here we have defined the possible states of the objects in our system for documentive purposes. Once we have an idea of what the possible application states are we can start describing what the inputs to the system will be.
Note that we define inputs up front and do not dynamically create mutable view objects that start randomly mutating models at run-time.
// Inputs
var addTodoPool = EventPool("add-todo")
var toggleAllPool = EventPool("toggle-all")
var modifyTodoPool = EventPool("modify-todo")
var clearCompletedPool = EventPool("clear-completed")
var routes = Router()
In our application we have defined for event pools that DOM events will eventually send messages too
and have also created an instance of a Router
(which uses hashchange) so that we can treat URI changes
as an input.
Once we have declared our inputs we should define how changes in an input allow for changes to happen in the application state.
// State operations
var addTodo = transform(addTodoPool.signal, function (todo) {
return function (state) {
return todo.title === "" ? state :
extend(state, { todos: state.todos.concat(todo) })
}
})
...
var handleRoutes = transform(routes, function (routeEvent) {
return function (state) {
return extend(state, { route: routeEvent.hash.slice(2) || "all" })
}
})
Here we have defined that we should transform the state of the AllTodo message pool into a function which takes our current state and returns a new state. In this case we check whether the new todo has a non-empty title and if so we just concat it onto the list.
We have also defined a transformation for the state of the routes
. Which again
is a function that takes current state and just updates the route property (or
defaults it to "all").
We would continue to write transformations for each one of our inputs that describe how to turn the input state into a change to our application state. You can find further example of transformations in the application source
So if we have these Signals of modification functions, how do we turn that into an actual representation of the application state?
var input = merge([addTodo, modifyTodo, toggleAll,
clearCompleted, handleRoutes])
var storedState = localStorage.getItem("todos-graphics")
var initialState = storedState ? JSON.parse(storedState) : TodosModel
// Updating the current state
var todosState = foldp(input, function update(state, modification) {
return modification(state)
}, initialState)
todosState(function (value) {
localStorage.setItem("todos-graphics", JSON.stringify(value))
})
In our case we merge together all these modification functions into a single Signal
.
We make sure we get a description of initialState
either from the model we defined
earlier or from local storage if we have one.
We then take our initialState
and each modification function and just accumulate the
representation of our current state.
Note we also listen on changes to the appState
and write them to localStorage.
We now have a signal that describes our entire application state! Now how would you go about building a UI from that? Could you, maybe just transform it into some kind of representation of a scene?
// Various template functions to render subsets of the UI
function mainSection(state) {
var todos = state.todos
var route = state.route
return h("section.main", { hidden: todos.length === 0 }, [
toggleAllPool.change(h("input#toggle-all.toggle-all", {
type: "checkbox",
checked: todos.every(function (todo) {
return todo.completed
})
})),
h("label", { htmlFor: "toggle-all" }, "Mark all as complete"),
h("ul.todo-list", todos.filter(function (todo) {
return route === "completed" && todo.completed ||
route === "active" && !todo.completed ||
route === "all"
}).map(todoItem))
])
}
...
var app = transform(todosState, function display(state) {
return h("div.todomvc-wrapper", [
h("section.todoapp", [
headerSection,
mainSection(state),
statsSection(state)
]),
infoFooter
])
})
We transform our application state using a display function that just builds up a representation
of what our app looks like visually. In our case we use a h
function that's similar to
hyperscript
as a terser equivalent for DOM templating. This approach works for any
templating system.
In our mainSection
function we do useful things like only showing the sections if we have todos
and we also decorate our toggle all button with a change listener from the toggleAllPool
. This
means that whenever a change event happens on the toggle button a message get's send to
the toggleAllPool.signal
.
We can write very simple obvious logic like saying that the toggle button should only be checked
if every todo in our list is completed. To create a todo item to render we can just do
todos.map(todoItem)
where todoItem
is just a single javascript function that returns
a representation of the todo state.
// Render scene
render(app, false)
To actually make everything flow cleanly we call render
on our application which use an efficient
diff based rendering system to avoid the computational overhead of recreating large chunks of the
scene each time our application state changes.
If you look at our application we never had to go mutate things or set up event handlers or write code like "if this things happen then do that thing". There is no glue code. There is just a set of pure transformations that take old values and return new values without mutating
- We transform our inputs into transformation functions and then merge those together and apply them on current state. We could this more cleverly and write transformation functions that only update a subset of the state, we can then delegate from our foldp to a subset and avoid having to recompute large part of the state each time. This could scale in setting up a recursive structure for describing your application state where each subsection has it's own sets of inputs it cares about.
- We use
h
as a nice pure JavaScript templating mechanism that has all the expressive-ness of JavaScript but still looks similar to HTML templates. This gives us our pure FRP style benefits whilst still being able to use CSS. - We use event pools to get around the chicken and egg problem of dynamic views wanting to
create dynamic inputs that should effect the state. We declare a finite set of event
pools upfront and those event pools use event delegation on the
document
internally.
The inputs & state transformations are written in a reasonably clean fashion.
The part of the example that's terribly ugly is all the display logic
- How do you avoid recreating an entire scene object each time the state has a small change? Maybe we can use something similar to the transforming things into functions that operate on state. This is a MASSIVE blocker on production usage.
- There is far too much logic in the templates. Is there an intermediate layer we can move it to.
- The pool api will probably need some polish. It's usage to wrap elements doesn't look as nice as wanted.
- How can we split up all the rendering logic, updating logic & inputs into multiple files?
Functional reactive programming sounds like the subset of constraint solving where the only types of constraints you are allowed to use are functional (and so solving stuff is trivial). Have you seen this project? I think it does what you are describing pretty much exactly:
https://github.com/soney/constraintjs
Also regarding visualization, it FRP might work a lot better with WebGL for the rendering, since the drawing model is way simpler. (ie you basically have a function which gets called every 16ms that redraws the screen from scratch)
Anyway, just my 2c.