A simple implementation of lens using type script
npm install @focuson/lensImmutable data structures are becoming the norm in both front end and server side code. Javascript and typescript have
become better at code for handling immutables. Projects such as 'immer' have arisen to help handle this.
This project offers a very simple way to handle one of the most important tasks: accessing deep parts of data structures
and perhaps more importantly offering easy ways to mutate them. The code is very small and light weight with no
dependancies (other than devDependencies)
* https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe
* https://medium.com/javascript-scene/lenses-b85976cb0534
* https://www.linkedin.com/pulse/functional-lenses-javascript-vladim%C3%ADr-gorej/
You can install this project by
``shell`
npm install @focuson/lens
To keep the projects small, the tests have been moved here
We'll start with examining how we work with a deep data structure. Here we can see a block of json representing a
dragon. In the dragon's stomach is some hapless adventurer. Our job is to write the eat method which will return a copy
of the dragon with the parameter added to the existing contents of the stomach
`typescript
export let startDragon: Dragon = {
body: {
chest: {
hitpoints: 10,
stomach: {
contents: ['the adventurer']
}
},
leftWing: {hitpoints: 5},
rightWing: {hitpoints: 4}
},
head: {hitpoints: 3, leftEye: {color: "blue"}, rightEye: {color: "green"}}
}
`
Let's write it without Lens first. And once we've written it, let's consider how fragile this code is. Imagine how easy
or hard it would be to change if the structure of the dragon changed.
`typescript
export function eat(dragon: Dragon, item: any): Dragon {
return {
...dragon,
body: {
...dragon.body,
chest: {
...dragon.body.chest,
stomach: {
...dragon.body.chest.stomach,
contents: [...dragon.body.chest.stomach.contents, item]
}
}
}
}
}
`
Now let's try writing it with Lenses. And once we've written it, let's consider how easy or hard it would be to change if the structure of
the dragon changed
`typescript`
export let dragonContentsL: Lens
Lenses.build
focusOn('stomach').focusOn('contents')
export let eat: (dragon: Dragon, item: any) =>
dragonContentsL.transform(oldContents => [...oldContents, item])(dragon);
The idea behind lenses is that they are 'focused' on a small part of a big object. In the example above the lens dragonContentsl was a lens that
given the 'main object' (a Dragon in this case) is focused on just the contents of the stomach. The lens allows us easy access to the thing that is focused
on, and allows us to change easily (in the sense of making an immutable copy with the changes) the focused part
In the example above dragonContentsL.transform(oldContents => [...oldContents, item]) creates a function that goes from a dragon to a new dragon. Theitem
only difference between the new dragon and the input dragon is the transform function applied to the contents. In otherwords it
returns a new dragon, identical except that new one has added to oldContents
A lens has the signature Lens[Main,Child and is two functions. One is a getter of signature (m: Main) => Child. The other is the setter which(m: Main, c: Child)=> Main
takes an original main, a new child and returns a new main. .
typescript
export let dragonContentsL: Lens =
Lenses.build('dragon').focusOn('body').focusOn('chest').
focusOn('stomach').focusOn('contents')
`
Here we started with a lens that was focused on the whole dragon (not the most exciting lens), and then gradually 'focused in' on
the place of interest (the contents of the stomach). This works because lenses are composible. If I have a Lens[Main,Child] and a Lens[Child,Grandchild]
I can smash them together and create Lens[Main,Grandchild. Intuitively we can just thing 'focuses in on the next part'Another example can be seen here:
`typescript
let msgToCupLens = Lens.build('msg').focusOn('order').focusOn('cup')
let cupToMadeofLens = Lens.build('cup').focusOn('madeOf')
let msgToMadeOfLens = msgToCupLens.andThen(cupToMadeofLens)
`How can I use Lens
Changing immutable structures
(With the usual meaning of getting a copy with the difference). The above example shows a simple way to do thatDecoupling the structure from business logic
A good example of this is in the examples code examples/lens/dragon. Here we can see the 'damage' method is handed a lens.
The damage code knows nothing about the structure of the dragon, it just knows how to go from a dragon to the hitpoints.
This approach of decoupling the business logic from the structure leads to code that keeps on working even when there are
dramatic changes to the domain structuresUpdating multiple parts simultaneously
Imagine we have a data structure and we need to update two parts at the same time. In the tictactoe example we need
to change the state of the current square and the value of next state.
We can do this quite easily by having a lens to 'part1' and a second one to 'part 2', then using the method
updateTwoValues or
transformTwoValues. The result is a new main with both modifications applied.$3
It is common for us to want to change the structure of our data. Without lens the impact can be very high: both in the
code and in the tests. With lens we can isolate the changes from the business logic, which means that typically we only
have to make very few changes when we makes changes to the structure. Without lens if we aren't extremely careful (which
may require us to program in a way that isn't idiomatic javascript/typescript)
we couple all the business logic to the structure.
If you want to play with the difference and experience it for yourself the
dragon` example project is a great place to