Cycle.js
A glimpse into
a simple FRP micro-framework
Cycle.js Agenda
- how we got here
(where are we and why is this even a thing) - evolution of callbacks, promises to streams (or observables)
- functional programming (purity)
- mutating state (can you localize it)
- putting a (simple) cycle.js app together
- wrap up w/ the goods & some bads
Web site complexity is not going away.
Files | Scripting |
---|---|
static content served | static content served |
dynamic server content | |
AJAX | Sockets |
---|---|
static content served | static content served |
dynamic server content | dynamic server content |
dynamic Data | dynamic Data |
interactive UI |
-- time --> (Browserzoic) today
(and genies don't go back in bottles)
Successive genies have been let out of the bottle
early web (age of the server)
how we got here
Browser Concerns
Client side coding has to interpret:
- Intention of the user (via incoming events)
- Update the model (according to some business logic)
- Render a view (change the DOM quickly)
Coding this stuff could get messy!
maybe MVC or WebComponents will help.
Add in some services, filters, directives, routers and more... (is this helping?)
To all this, Cycle.js says "Try FRP (functional reactive programming) with streams."
how we got here
Don't see anything new here
Of course there is nothing new here. Cycle.js makes web apps and there are no big reveals here. The output of this code is just "another web app."
This is completely different
Cycle.js gives a complete new perspective on developing UI interaction and makes you take a new approach to coding up just
"another web app."
Is Cycle.js Different?
Powered by streams, functional programming and a simple architecture that isolates all side effects. Down the rabbit hole we go.
Callbacks
non-blocking for async
evolution of callbacks, promises to streams
Yea, but callbacks have some problems when you try to make a layered solution. It's difficult to augment a callback with preprocessors. Callbacks will run in the scope (closure) that the function was declared in.
Promises
chaining data processors
Promises allow many preprocessor to each run in the scope that you want.
Nice, but promises are a one shot deal. They handle a request to a server or a long running calculation and then they are done.
What if you have many promises overlapping (as in "autocomplete")? We need a way to coordinate a sequence of values.
evolution of callbacks, promises to streams
streams
like an array of values that never ends
with lots of useful functions to manage them
A Stream of values over time.
You can subscribe to a stream and react to changes, you can filter, map, reduce and scan or merge them.
also called Observables
evolution of callbacks, promises to streams
pure functions
Pure functions must:
- only access their arguments
(no reaching out of scope. Not even using "this.") referential transparency - return a value
// no chance this is pure
void main() { ... }
// either it has side
// effects or it does
// nothing
// better, but not pure
int main(args) { ... }
// returning an error code
// is not the product
// has a chance of being pure.
stdout main(stdin) { ... }
functional programming
pure benefits
- testable, dependable (given the same input they always produce the same output)
- mapping, composing, recursive and other higher order functionality
- properties of translation on function can be used associative commutative ...
- no dependency on scope
- efficient in parallel processing environments
functional programming
functional programming
OO
"Smart" objects
Pass data around
Imperative
Imperative programming changes values as soon as possible, writing to small parts of an application's state as needed.
Objects manage this in a controlled way through setter functions etc.
Functional
Functional programming only mutates the application state after a complete (well formed) state is calculated.
Functions never make assignments outside of their scope.
So, No Side Effects!
ways of managing state
writing to the app's own internal data
mutating state
Side Effects
Side Effects have to happen or your app would never change state (i.e. never do anything).
Assignment make the imperative world go around.
Pure FRP
Pure Functions have no side effects, Right!
But wait, without side effects, applications could not exist (at least not as we know them)!
Some place in an FRP app there has to be some side effects, but where?
But wait, you have to have side effects
mutating state
The side effects are out there
FRP makes a new state without partially setting the state.
calculate a new state
-
arguments contain everything needed
-
local assignments
- returns result of calculation
the result is transactional - It either succeeds, or it fails and no state is left halfway updated (no crashing in the case of Elm).
mutating state
f(x)
FRP frameworks collect all side effects in one place
Well that is the ideal. Some FRP frameworks only encourage this, while others insist on it and don't allow any mutation at all.
- Redux, suggests it
- HyperApp.js encourages
- Cycle.js encourages and makes it easy to conform.
- Elm-lang insist on it!
mutating state
sinks = main(sources) {
//your code here, keep it pure.
}
putting a cycle.js app together
import {div} from '@cycle/dom'
import xs from 'xstream'
function main (sources) {
const vtree$ = xs.of(
div('My First Cycle.js app')
)
const sinks = {
DOM: vtree$
}
return sinks
}
putting a cycle.js app together
function main(sources) {
const decrement$ = sources.DOM
.select('.decrement').events('click').map(ev => -1);
const increment$ = sources.DOM
.select('.increment').events('click').map(ev => +1);
const action$ = Observable.merge(decrement$, increment$);
const count$ = action$.startWith(0).scan((x,y) => x+y);
const vtree$ = count$.map(count =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count)
])
);
return { DOM: vtree$ };
}
putting a cycle.js app together
putting a cycle.js app together
import {div, button, input, p} from '@cycle/dom'
import xs from 'xstream'
export function App (sources) {
const text$ = sources.DOM.select('.atext').events('keyup')
.map(ev => ev.target.value).startWith('hi');
const toga$ = Toggle(sources, '.atoggle');
const togb$ = Toggle(sources, '.btoggle');
const vdom$ = text$.map(text =>
div([
input('.atext'),
p("test"+JSON.stringify(text))
])
);
const compose$ = xs.combine(toga$, togb$, vdom$)
.map(([Toga, Togb, VDom]) =>
div([Toga, Togb, VDom])
);
const sinks = {
DOM: compose$
}
return sinks
}
reading sources and sending out a new DOM(a text input and two checkboxes)
// Toggle is a component that takes a source and returns a sink
function Toggle(sources, id) {
const toggle$ = sources.DOM.select(id).events('change')
.map(ev => ev.target.checked)
.startWith(false);
const togglesink$ = toggle$.map(toggled =>
div([
p('hello from a toggle.'),
input(id, {attrs: {type: 'checkbox'}}), 'Toggle '+id,
p(toggled ? 'ON' : 'off')
])
);
return togglesink$;
}
wrapping up with the goods & some bads
- new tools are being created to allow you to see the stream structure in your app, they reads like a specification of the app https://github.com/cyclejs/cyclejs/tree/master/devtool
- Streams are re-playable so you get great bug reports or you can use a time traveling debugger.
- Intent, Model, View is only one way to cut the problem up. You are free to choose the best way for your app.
for example, see: cycle-onionify and cycle-collections - the core is so small and simple that it is not likely to change to much
the GOODS
wrapping up with the goods & some bads
- cycle.js is immature
- Streams are hard to wrap your head around (there is a learning curve)
- Still finding a way to make common components interchangeable...
some Bads
If there is time we could grok this slightly more involved to-do-list example.
Cycle.js FRP
By Nate Morse
Cycle.js FRP
Jun 2017
- 1,019