its-alive
v0.4.1
Published
Dead simple FRP.
Downloads
40
Readme
itsAlive
Dead simple FRP.
Overview
itsAlive is an attempt to add tangibility to functional reactive programming (FRP) for the purpose of making FRP easier and more accesible to programmers who are new to it.
It does this by introducing the concept of living values - variables that are allowed to mutate but only do so in a controlled fashion. See documentation section for more details.
Installation
NPM
npm install --save its-alive
UNPKG https://unpkg.com/its-alive@0.3.0
Documentation
Introduction
#####The very basics
Let's start by thinking about assignment. In JavaScript, variables are assigned by value.
let a = 1
let b = a + 1
...
First, a
is assigned a value of 1
. Then...
...
... well it's tempting to think b
is assigned a+1
, but it is not. a+1
is evaluated to 2
and b
is assigned the result.
When a
is modified, the value of b
does not change...
...
a = 3
console.log(b) // still 2, not 4 (= a + 1 = 3 + 1)
...
If we want changes in a
to be reflected in b
, we could always rewrite b = a + 1
, but that kind of sucks.
#####We want to define a relationship between a
and b
, not simply assign b
a value
Let's leave JavaScript world and describe the relationship in Math world. In Math world, the sign is NOT assignment. It means is (forever! even after changes)...
We can even abstract out the "doing stuff" part of the relationship and call it a function, , and let be some generic input...
... and then rewrite the relationship between and to be more concise...
If we go back to JavaScript world, we can write an addOne
function, but unfortunately, this does not fix our issue of assignment - assignment passes a value, it does not define a relationship.
function addOne(x) { return x + 1 }
let a = 1
let b = addOne(a)
a = 3
console.log(b) // still 2, not 4 (= a + 1 = 3 + 1)
So what problem are we trying to solve? We want to define a relationship between two variables! We do not want to simply evaluate an expression (possibly containing variables) and assign the result to a new variable.
#####Elements of a relationship
Let's breakdown out mathematical relationship from the above section. We had...
- : the variable being defined
and
- : a function that takes an input value and modifies it in some way
- : represents the input to f(x)
- : the variable we are using to define b
So what is b
? It is f(x)
where a
is fills in the input spot x
.
#####**It's Alive! **
Let's use itsAlive to define the relationship between a
and b
...
function addOne(x) { return x + 1 }
// initialize a to 1
const a = itsAlive(1)
// define relationship between a and b
const b = itsAlive()
.setReducer(addOne) // use the addOne function for f(x)
.listenToInput(a) // map a to the x (first) input spot
a.update(3)
console.log(b.valueOf()) // b is 4! Hurrah!
So, when using itsAlive, you explicity define a "living" value as a function and it's inputs - this pairs up perfectly with the mathematical elements of a relationship that we defined above.
Basics
#####What is a "living" value?
A "living" value is a reactive variable that is made up of the following components
- a cached value (a number, string, boolean, null, or object, including arrays and functions; typically not undefined)
- a reducer function
- a set of inputs to the reducer function (which can also be listened to)
- a set of values to listen to
These components are grouped together in an object returned by the itsAlive factory function itsAlive()
. If you inspect the living value directly, you will see the object. The value can be accessed through the .valueOf()
method (which is automatically called when using most binary operators).
const a = itsAlive(3)
console.log(a) // object with a bunch of props/methods
console.log(a.valueOf()) // 3
console.log(+a) // 3 - the `+` operator called `.valueOf()`
console.log(a+1) // 4 - the `+` operator called `.valueOf()`
Those pieces that make up the living value can be set using the methods on the living value.
function addOne(x) { return x + 1}
const a = itsAlive(3)
const b = itsAlive(0) // set initial cached value to 0
.setReducer(addOne) // use addOne as the reducer function
.setInput(a) // use the value of `a` as the input to addOne
.listenTo(a) // update `b` when `a` updates
// the convenience function `.listenToInput(x)` is a shorter way
// to write `.setInput(x).listenTo(x)`
...
Note that after the above code runs, a
is 3
and b
is 0
. At first this might seem strange, but living values have to be explicitly updated! If you're wondering, "then what the hell is the point?", then hopefully its worth noting that when a
updates, it automatically notifies all values listening to it to update as well.
...
console.log(b.valueOf()) // 0 - not 3 because `a` or `b` have not been updated
...
#####Updating a living value
You can update a living value by calling it's .update()
method.
- Without a supplied value the inputs are applied to the reducer and the result is stored as the new value
- With a supplied value the reducer is bypassed and the supplied value is stored as the new value
Any time a living value is updated, it automatically updates all values listening to it.
function addOne(x) { return x + 1}
const a = itsAlive(3)
const b = itsAlive(0) // set initial cached value to 0
.setReducer(addOne) // use addOne as the reducer function
.setInput(a) // use the value of `a` as the input to addOne
.listenTo(a) // update `b` when `a` updates
b.update() // 4 -- the value of `a` (3) was applied to
console.log(b.valueOf()) // the addOne reducer function
a.update(5) // 5 -- explicitly set `a` to 5
console.log(a.valueOf())
// because `b` was listening to `a`, `b.update()` was called, applying the new value of `a` to the `b` reducer function, addOne.
console.log(b.valueOf()) // 6
Why are .setInput()
and .listenTo()
separated?
If you want to create a relationship between two variables, like...
... you want to both set a
as an input and also listen for changes in a
. Why have two methods for this and not just use something that does both, something like a .listenToInput()
method. Well, actually, .listenToInput()
is available as a convenience method, but it's just a shorter way to write .setInput().listenTo()
.
So why are they separated? This gives you the additional option of updating a living value independently of it's inputs. One possible use case for this is that a value can be dependent on itself without infinitely recursing!
function addOne(x) { return x + 1}
const a = itsAlive(3)
const b = itsAlive(0) // set initial cached value to 0
b.setReducer(addOne) // use addOne as the reducer function
.setInput(b) // use the current `b` value to calculate new one
.listenTo(a) // update `b` when `a` updates
a.update(5)
console.log(b.valueOf()) // 1
a.update(7)
console.log(b.valueOf()) // 2
a.update(9)
console.log(b.valueOf()) // 3 -- `b` uses addOne to increment ITSELF
// each time `a` is updated
See the advanced section on Reduce for more.
Values cannot be undefined
Living values cannot be undefined. If you want to represent that the value has no value, use null
. So what happens if the reducer returns undefined
?
It does nothing. It does not update the value. It does not notify it's listeners.
See the advanced section on Filter for more.
Synchronous updating
to be written...
Asynchronous updating
to be written...
Advanced
Freezing a value
Frozen values cannot be updated. In turn, values listening to the frozen value will not be notified/updated.
function addOne(x) { return x + 1}
const a = itsAlive(3)
const b = itsAlive(0) // set initial cached value to 0
.setReducer(addOne) // use addOne as the reducer function
.setInput(a) // use the value of `a` as the input to addOne
.listenTo(a) // update `b` when `a` updates
a.freeze()
a.update(5) // ignored
console.log(a.valueOf()) // still 3
console.log(b.valueOf()) // still 0
a.unfreeze()
a.update(5) // sets `a` to 5, updates `b` to 6
console.log(a.valueOf()) // 5
console.log(b.valueOf()) // 6
Quieting a value
Quieted values can be updated, but will not notify/update values that are listening to it.
function addOne(x) { return x + 1}
const a = itsAlive(3)
const b = itsAlive(0) // set initial cached value to 0
.setReducer(addOne) // use addOne as the reducer function
.setInput(a) // use the value of `a` as the input to addOne
.listenTo(a) // update `b` when `a` updates
a.quiet()
a.update(5) // updates `a` to 5, `b` is not updated
console.log(a.valueOf()) // 5
console.log(b.valueOf()) // still 0
a.unquiet()
a.update(7) // sets `a` to 5, updates `b` to 6
console.log(a.valueOf()) // 7
console.log(b.valueOf()) // 8
Filter
As discussed here, when a values reducer returns an undefined
value, it simply does nothing. We can use this behavior to design a reducer to selectively fail to update in certain conditions, creating a filter.
function addOne_if_under10(x) { if( x < 10) return x+1 }
const a = itsAlive(3)
const b = itsAlive(0) // set initial cached value to 0
.setReducer(over10) // use over10 as the reducer function
.setInput(a) // use the value of `a` as the input to addOne
.listenTo(a) // update `b` when `a` updates
a.update(5)
console.log(b.valueOf()) // 6
a.update(7)
console.log(b.valueOf()) // 8
a.update(2500)
console.log(b.valueOf()) // still 8 -- ignored change since a >= 10
Alternatively, we can separate the addOne
and under10
logic by having b
be dependent on a
while listening to an intermediate value isUnder10
.
function addOne(x) { return x+1 }
function under10(x) { if(x < 10) return true }
const a = itsAlive(3)
const isUnder10 = itsAlive()
.setReducer(under10)
.listenToInput(a)
const b = itsAlive(0)
.setReducer(addOne)
.setInput(a)
.listenTo(isUnder10)
a.update(5)
console.log(b.valueOf()) // 6 -- `under10` updated from true -> true
// this triggered `b` to update too
a.update(7)
console.log(b.valueOf()) // 8
a.update(2500)
console.log(b.valueOf()) // still 8 -- `under10` ignored change since a >= 10
Reduce
Reduce here is similar to Array.prototype.reduce()
. The reduce process taken, step by step, combines an "accumulated" value with a "current" value to obtain the next "accumulated" value.
You can use a living value to store your "accumulated" value and simply set the value to use itself as an input.
// Array.prototype.reduce -- does not use itsAlive
function accumulate(sum, currentValue) { return sum += currentValue }
let sum = [1,2,3].reduce(accumulate, 0) // 6
// itsAlive reducing -- there's some added boilerplate to set up your living values, but updates can be synchronous or asynchronous
function add(a,b) { return a+b }
function logger(x) { console.log(x) }
const currentValue = itsAlive(),
sum = itsAlive(),
log = itsAlive().listenToInput(sum).setReducer(logger)
sum.setReducer(add)
.setInputs(sum, currentValue)
.listenTo(currentValue)
// synchronous updating
[1,2,3].forEach((x)=>currentValue.update(x)) // logs 1, 3, 6
// asynchronous updating
setTimeout(()=>currentValue.update(4), 2000) // logs 10 after 2 second
Buffer
To maintain a history of past values, simply create a living array that listens to a value whose reducer function pushes updates to the living array.
function logger(x) { console.log(x) }
const value = itsAlive(),
history = itsAlive([]),
log = itsAlive().listenToInput(history).setReducer(logger)
history
.setInputs(value, history)
.listenTo(value)
.setReducer((val, arr)=>{
arr.push(val)
return arr
})
// note: the logger is logging new values of `history` after each update
value.update(1) // logs [1]
value.update(2) // logs [1,2]
value.update(3) // logs [1,2,3]
You can modify the reducer on history
to enforce rules. For example, you could add a check like if(arr.length < 10)
to only take the first 10 values, or implement a queue structure to keep the 10 latest values.
Make itsAlive from scratch
to be written...
API
to be written...
Examples
to be written...
License
MIT