Telescope is another state management library for JS ecosystem, this time based on lenses. It's an alternative to Redux for the brave as it's still a baby learning to give its first steps.
A telescope is a special object that handle changes on a complex state by providing handlers to smaller pieces of the big structure, each of these handlers is a new telescope and they are created by magnifying up a special part of the whole thing. As with regular magnification, you need lenses to do this, so, we provide the lens and the telescope focus on what is needed.
So it can be said that a telescope is like a telescope (or a microscope) that focuses on fragments of a bigger universe and allows users to manipulate the whole universe through updates on the fragments.
A lens is a pair of functions, one to get values from an bigger object and another one from creating big objects with a piece and a previos big object:
The final implementation is a bit more loaded, but this is the essence.
Lenses can be composed, and this is one of their great features, once you can compose blocks of any type, you have a magical way to create new ones.
To have a more concrete idea of what is going on we can look into an example:
Suppose you have a bicycle and while doing some fun riding you get a flat tire. You know how to temporarily fix a tube, but you have no clue on how to get the tube out from the bike. Fortunately you have two friends, the black squirrel that knows how to get a wheel out from a bike and an avocet that knows how to get out a tube from a wheel, they of course know how to put things together.
So, how do you fix the tire? You give the bike to the black squirrel who returns you the wheel which you pass to the avocet who gives you the tube which you fix and give back to the avocet who gives you the wheel that you pass now to the black squirrel that finally returns you the whole working bike.
Let’s make that more clear with code:
;// Your super power to fix tubes: (Bike) => Bike;// A couple of lenses friends of you:// blackSquirrel: Lens<Bike, Wheel>// avocet: Lens<Wheel, Tube>;;// You start your ride with a shining bicycle.;// You subscribe to the ride in case something happens.ride.stream.subscribebike.hasFlatTire ? fixBikebike, ride : keepGettingFun// ... bunch of other things may happen.// Ask the squirrel for the wheel..magnifytireLens// Ask the avocet for the tube..magnifytubeLens// Fix the tube.// Telescope will pass the pieces back to your friends so you get back your bike in the stream..evolvefixTube;
The good part about this all is that the main pieces of functionality are clearly separated and can be easily tested:
- Evolutions, which are or should be pure functions, where you update the small fragments of your state.
- Lenses, which are or should be pairs of pure functions (
setter), where you extract fragments and put back together the bigger values.
As an extra bonus, lenses can be composed so you can zoom in and out using them.
Telescope lenses are special machines that can transform an evolution on a type of values
A into another evolution on type of values
B. That is, they are kind of a function from
Evolution<B>. Let’s write them as:
Lens<A,B> kinda Evolution<A> ~> Evolution<B>
And as functions they can be composed as long as the types matches):
Evolution<A> ~> Evolution<B> ~> Evolution<C> // Becomes: Evolution<A> ~> Evolution<C>
As in regular typescript functions, there is no special operator for composition, but Telescope lenses are equipped with the method
compose which does what we expect. In our previous example we can now do a small refactor:
// …// Ask the squirrel and the avocet for the tube..magnifytireLens.composetubeLens// Fix the tube.// Telescope will pass the pieces back to your friends so you get back your bike in the stream..evolvefixTube;
Having composition is actually a big thing which allows for: reducing testing scenarios as only small lenses need to be tested, composite lenses are as good as its elements; and decoupling responsibilities as lenses can live in the code closer to the data structures they handle.
We’ll see how to use Telescope in a simplified TODO application. Let’s start with a general domain definition agnostic of any framework:
// A type for the global state.
With actions or side effects:
// Given a Telescope on a Todo, toggle it.;;;;
And one Lens:
This example shows how to integrate Telescope in a React application using the Redux style which is to have a global state. The state is represented and maintained by a single stream inside a Telescope. We’ll use lenses and magnification only to keep a handle for our side effects.
As a bootstrap for the application we need something like:
From then on, we don’t even need to subscribe to the stream, react will handle it for us.
In React it will be easier to do state projections directly against the
props of the components, but you can for sure use the getters.
Although this example is pretty simple and not even includes Lens composition, it shows the basics on how to integrate Telescope with React.
Just as with React, but assuming Angular instead :)
As a bootstrap for the application we need something like:
Here is the template for the main component, it can be optimized, but still you can see the simplicity:
From then on, we don’t even need to subscribe to the stream, using the async pipe, Angular will handle it for us.
To render the Todos in a hierarchical mode we rely in the following component:
Note that we need to wrap the actions in instance methods since the controllers in angular serve as a namespace and we cannot refer top level functions from the template.
What do you need to do?Add
And its template:
As you can see, the Angular version is a bit more verbose but just like in the React counterpart, the controllers are very simple in terms of logic, which results in components easier to test and the same is true for the business logic.
This story goes more or less as you would expect: How to handle changes on state in an application where many actors need to react to those changes and many actors are producing those changes?
The idea behind Telescope is that each change can be seen as a delta to the previous state, and the first challenge we face is that unless the state is some kind of number-like value, it is not clear what a delta is in this context. The key is that we don’t really need to know what this delta is nor how to add it to the previous state value, we can trust Telescope users to know what to do and encapsulate the delta and the adding process in a function, a function that will take a state value and return a new one:
We call these type of functions
Evolutions but they have other names too, in Mathematics they are commonly known as endomorphisms.
There is a caveat of course. As there are many values for this deltas and maybe different adding processes depending on the particular interaction happening in the application, there will be potentially many evolutions. In Telescope this is OK, evolutions are treated as regular values.
The Telescope idea comes from answering this question: What do we do with all these evolutions to get back our states?
The first observation we can do is that these evolutions are not just given, they are pumping up as the user, other systems or even internal processes interact with our application.
The way Telescope deals with this is a common pattern: Streams. But these are special streams, they are streams of evolutions, which is, streams of functions, we still need to figure out where to get values again.
Fortunately, streams belong to a big family of things that can be folded and one special way of folding is to generate a new foldable with the partial results of the folding steps. This folding is commonly named
Putting all together means to take the stream of evolutions and scan through it providing an initial state value as a seed (this value may come from a database or simply be a default state). And this is what a telescope is: a convenient wrapper for a stream of evolutions so we can convert them into a stream of values.
The second part of this story is about how we can interact with these telescopes and how we can create new ones from existing ones. That is, how can we create a world that telescopes can inhabit?
Telescopes can be seen as consumers of values of a given type, let’s say
U and produce values of the same type
U through a stream. In order to convert a Telescope of
U’s to a Telescope of
P’s, we’ll need to provide two functions, one from
P and another one from
This is all fine as long as this functions are inverses one to the other, but in many situations we want to convert the original
U type into a smaller
P, that is,
P has lees information than
U. We do this through Lenses, a special attraction originally intended as a data accessor for nested structures. When we use a Lens to transform a Telescope, we say we do a magnification, that is, we use the given Lens to look only into the details that will be represented in the type
Lenses most common representation was described and implemented by Edward Kmett for Haskell (github@ekmett/lens) and since then have been getting more and more attention as a powerful abstraction for composable accessors.
Lenses are one of many other optics and while we only use lenses in Telescope, we are planning to add Prisms and other toys to the box.
The code in this library is pretty small but we still need a minimal pipeline for testing, building and distributing.
_bundles/ // UMD bundles coverage/ // Coverage reports lib/ // ES5(commonjs) + source + .d.ts lib-esm/ // ES5(esmodule) + source + .d.ts node_modules/ // You know what this is src/ // All Telescope source code is here package.json README.md tsconfig.json …
build:commonbuilds the CommonJS files (TSC)
build:es6builds the ES6 files (TSC)
build:umdbuilds the UMD files (Webpack)
cleandeletes generated files (i.e
test:druns tests in interactive mode
testruns all tests
tslintruns the linter.
- Jasmine is a test framework for JS. jasmine.github.io.
- Karma is a test runner. karma-runner.github.io.
- RxJS is a library for Streams, Sinks and other reactive toys. rxjs-dev.firebaseapp.com. This is the only peer dependency for Telescope, all other stuff is used only for the developers contributing to Telescope itself.
- TSLint is a linter for Typescript. palantir.github.io/tslint.
- Webpack (and Webpack-CLI) is a bundler tool commonly used in front end projects. We use it here as a helper for running tests and to transpire UMD modules. webpack.js.org.
- Awesome Typescript Loader allows easy transpilation of Typescript code. github@s-panferov/awesome-typescript-loader.
- Istanbul Instrumenter Loader remaps coverage reports using source maps. Used only here for coverage reports. github@webpack-contrib/istanbul-instrumenter-loader.
- Source Map Loader generates source maps from transpilation.
- G. Boisseau and J. Gibbons, What you needa know about yoneda: profunctor optics and the yoneda lemma (functional pearl), Proceedings of the ACM on Programming Languages, 2 (2018), p. 84.
- M. Botto. Compiling and bundling TypeScript libraries with Webpack.
- S. P. Jones, Lenses: compositional data access and manipulation, October 2013.
- E. Kmett, Lenses, folds and traversals, in Talk at New York Haskell User Group Meeting, 2012.
You will find a bibTex file in this repo in the remote case you need or want to use it.