This is the documentation for the upcoming v1.0.0.
Documentation for v0.x.x is available here.
This library provides a TypeScript
implementation of an "Applicative Data-Driven Computation"
described by Conal Elliot in his paper.
The implementation:
- is a push-pull model with atomic updates (means it is glitch-free, no diamond shape problem);
- follows calmm architecture;
- is strictly typed.
Table of contents
Please refer to the Updates section for more info on the changes in the API.
Functional reactive programming is hard.
Coming up with a memory-leak-free, glitch-free, straightforward and intuitive implementation is even harder.
The goal of this library is to try to provide users with such implementation balancing between purity and ease of adoption
while still being fully type-safe, correct and TypeScript
-oriented.
The library describes the concept of a "value-over-time".
Basically it's a value that may change over time and subscribers can listen to updates of that value.
Such values are not described by Behavior
s as a function of time from classical FRP, but by a mutable reactive Atom
s.
On the other hand the library doesn't try to replace existing implementations of Observer
pattern such as rxjs, most or others.
Instead, it adopts some advanced FP concepts like HKT
(higher kinded types) and tagless final
to allow using properties and atoms with any implementation of Observable
.
This is where fp-ts comes into play.
So if we refer to the paper, it highlights two main concepts for working with reactive data-driven computation: value extraction and change notification:
Imperative programs implement data-driven computation using two mechanisms: value extraction and change notification. Value extraction allows retrieval of a “current value” (e.g., via an input widget’s access method). Notification allows various states (e.g., an output widget) to be updated, making them consistent with newly changed values.
The rest of the doc describes this API in details.
The library core consists of the following pieces which are borrowed from calmm architecture:
-
Observable
,Observer
andSubscription
- the basic building blocks ofObserver
-pattern, which adhere es-observables. -
Property
- describes a reactive "value-over-time".Property
extendsObservable
in the way that it always has a value.Property
allows getting its current value while still notifying its subscribers about changes of that value. A typical property could describe a value of aninput
, a count of clicks, current date etc. This is the main difference fromObservable
which just describes an occurence of some event. -
Atom
- describes a mutable reactive "value-over-time".Atom
extendsProperty
in the way that is allows mutating current value still being capable of everythingProperty
is capable of - holding the value and notifying about its changes.
This section of the doc aims to give a better understanding of how the things work. So to achieve that we'll build up a simple counter, reviewing all pieces of the design one-by-one.
Let's skip Property
and move straight to Atom
because essentially Property
is
just a readonly Atom
and mutability is required for the next example.
So, Atom
adds mutability to Property
. Let's create one:
import { atom } from '@frp-ts/core'
// We create an atom that will allow us to get values, update its value manually and listen to updates.
const counter = atom.newAtom(0)
// get the last value
console.log(counter.get()) // logs '0'
// let's manually set the value
counter.set(1)
// get the last value
console.log(counter.get()) // logs '1'
// or we can modify instead of setting
counter.modify((n) => n + 1)
// get the last value
console.log(counter.get()) // logs '2'
That's it. Pretty easy, huh? What about updates?
// subscribe to updates
counter.subscribe({
next: () => console.log(`value: ${counter.get()}`),
})
counter.set(3) // logs 'value: 3'
We're done but there are two important things about the callback:
- it is in the form of
Observer
. This is becausefrp-ts
implements es-observables so that it is seemlessly compatible with other implementations. Also, there's no support for plain functions as callbacks for the sake of API simplicity. - it is not fired on subscription. This is because, although
Atom
(andProperty
) is similar to rxjs BehaviorSubject, it is fundamentally different in the way it works - it only notifies subscribers if the value is changed. - it is not supplied the changed value. This is how
frp-ts
solves glitches (diamond shape problem). There should always be a single source of truth for the value - either it is the callback's argument (like it's done in almost all reactive libraries) or the value of property/atom.
As it was said before that Property
is a readonly Atom
, or vice-versa Atom
is a mutable Property
, there's actually nothing more to add on Property
.
Properties are introduced as a way to restrict API.
Sometimes we don't want to expose mutable access to our state and Property
is a perfect fit for such situations.
Now let's update our counter to restrict arbitrary mutations of its state:
// we'll need an interface to describe our counter more precisely
interface Counter extends Property<number> {
readonly inc: () => void
}
// we'll also need a constructor that takes initial value
const newCounter = (initial: number): Counter => {
// here we define local mutable state
const state = newAtom(initial)
// expose readonly API
const inc = () => state.modify((n) => n + 1)
return {
...state,
inc,
}
}
// now create counter and increment its value
const counter = newCounter(0)
counter.inc()
// note that no there's no direct access to internal mutable state of our counter anymore
This section covers advanced use cases covering lenses and integration with fp-ts.
Lensing is a feature that allows easier read/write access to deep nested structures stored in atoms.
Package @frp-ts/lens
includes a LensedAtom
interface which extends Atom
with a view
method.
Although lenses are not a part of @frp-ts/lens
, LensedAtom
supports them via an interface.
So Lens
is a combination of a getter and an immutable setter. Its interface is pretty simple:
interface Lens<S, A> {
readonly get: (s: S) => A
readonly set: (a: A) => (s: S) => S
}
Lenses are extremely powerful when it comes to immutable updates of deeply-nested structure. Let's try to build some example with lenses.
// let's define a nested structure
import { newLensedAtom, Lens } from '@frp-ts/lens'
interface Person {
readonly name: string
readonly age: number
}
// and create a person
const mike = newLensedAtom({ name: 'Mike', age: 20 })
// now what if we have a user interface that allows changing person's name and age,
// for example a form with two inputs?
// if we want to stay immutable we would need to deal with nesting
// each time we need to update nested value as well as read it:
const setName =
(name: string) =>
(person: Person): Person => ({ ...person, name })
const getName = (person: Person): string => person.name
const setAge =
(age: number) =>
(person: Person): Person => ({ ...person, age })
const getAge = (person: Person): number => person.age
// then somewhere further in some kind of callback
const handleNameChange = (newName: string) => mike.modify(setName(newName))
const handleAgeChange = (newAge: number) => mike.modify(setAge(newAge))
// this would quickly result in a lot of boilerplate
// and here lenses come into play
const nameLens: Lens<Person, string> = {
get: (person) => person.name,
set: (name) => (person) => ({ ...person, name }),
}
const ageLens: Lens<Person, number> = {
get: (person) => person.age,
set: (age) => (person) => ({ ...person, age }),
}
// now we can call `view` method which returns an `Atom`
// "zoomed" to a field defined by lens
const mikeName = mike.view(nameLens)
// note that subscriptions also work out of the box
mike.subscribe({
next: () => console.log('mike has changed'),
})
mikeName.subscribe({
next: () => console.log('name has changed'),
})
console.log(mikeName.get()) // logs 'Mike'
mikeName.set('Patrik') // logs 'mike has changed' and 'name has changed'
console.log(mikeName.get()) // logs 'Patrik'
Cool, but that's not all.
As view
method returns a LensedAtom
, then we can call view
multiple times in a chain, and it will result in a "lens composition"!
This means that we can nest reads and writes infinitely in a safe and predictable manner.
As mentioned above, @frp-ts/lens
does not ship with a Lens
implementation leaving it for an external library.
One of them is monocle-ts and you can always build an adapter
around any other implementation which is not compatible with the supported interface.
fp-ts
is a purely functional library that implements a "Light-weight Higher-kinded Polymorphism" in TypeScript.
This allows for many advanced FP techniques such as HKT (higher-kinded types).
The package @frp-ts/fp-ts
exports an instance of Applicative for Property
and pipeable
top-level functions.
It also exports some extra helpers for working with the library (e.g. map
, ap
, sequenceT
, sample
, sampleIO
etc.).
Please refer to the package documentation for more info.
@frp-ts/react
package provides a React hook to work with Property
values.
The hook immdetiately returns current value of a Property
and subscribes to updates via useEffect
.
frp-ts
is available as a set of npm
packages, neither of which require any external peer dependencies:
The core package (@frp-ts/core
) contains a minimal API required for working with the properties and atoms.
The lens package (@frp-ts/core
) contains APIs for working with lenses.
The fp-ts integration package (@frp-ts/fp-ts
) contains an integration layer with fp-ts
including HKT
registration and helpers for working with higher-kinded types.
Read more here
This repository is powered by nx.
This means that subpackages are built into a single directory /dist
.
This repository uses lerna ONLY for bumping versions until it's supported natively by nx.
Make sure NOT to call lerna bootstrap
and other commands.
lerna version <version>
Then
npm run deploy