Map/Reduce
Map
While by default an optic is focused on one value, and a partial
optic on zero or one value, a mapped optic is focused on multiple values.
A mapped optic can read its focused values, but also update every of them simultaneously.
To illustrate that let's create a new state holding US national parks:
ts
conststateOptic =createState ([{name : "Zion",visitors : 5_039_835,attractions : ["angels landing", "great white throne"],},{name : "Yosemite",visitors : 3_287_595,attractions : ["half dome", "el capitan", "mariposa grove"],},{name : "Yellowstone",visitors : 4_860_242,attractions : ["old faithful", "grand prismatic"],},]);
ts
conststateOptic =createState ([{name : "Zion",visitors : 5_039_835,attractions : ["angels landing", "great white throne"],},{name : "Yosemite",visitors : 3_287_595,attractions : ["half dome", "el capitan", "mariposa grove"],},{name : "Yellowstone",visitors : 4_860_242,attractions : ["old faithful", "grand prismatic"],},]);
The initial state is an array so we can call .map()
to get a mapped optic, an optic that maps over the values of the array:
ts
constnationalParksOptic =stateOptic .map ();
ts
constnationalParksOptic =stateOptic .map ();
From there we can get the name of every national park ...
ts
constnamesOptic =nationalParksOptic .name ;namesOptic .get (); // ['Zion', 'Yosemite', 'Yellowstone']
ts
constnamesOptic =nationalParksOptic .name ;namesOptic .get (); // ['Zion', 'Yosemite', 'Yellowstone']
... or update them all at once:
ts
namesOptic .set ((prev ) => `${prev } National Park`);namesOptic .get (); // ['Zion National Park', 'Yosemite National Park', 'Yellowstone National Park']
ts
namesOptic .set ((prev ) => `${prev } National Park`);namesOptic .get (); // ['Zion National Park', 'Yosemite National Park', 'Yellowstone National Park']
Same thing but for the annual number of visitors:
ts
constvisitorsOptic =nationalParksOptic .visitors ;visitorsOptic .get (); // [5_039_835, 3_287_595, 4_860_242]visitorsOptic .set ((prev ) =>prev + 1_000_000);visitorsOptic .get (); // [6_039_835, 4_287_595, 5_860_242]
ts
constvisitorsOptic =nationalParksOptic .visitors ;visitorsOptic .get (); // [5_039_835, 3_287_595, 4_860_242]visitorsOptic .set ((prev ) =>prev + 1_000_000);visitorsOptic .get (); // [6_039_835, 4_287_595, 5_860_242]
The update function is applied to every focused value, which is convenient to update or reset a large number of values all at once.
The new optics you derive from a mapped optic are themselves mapped optics !
Multi level
You can call .map()
every time the focused value is an array, even if the optic is already a mapped one:
ts
constattractionsOptic =nationalParksOptic .attractions .map ();
ts
constattractionsOptic =nationalParksOptic .attractions .map ();
Here attractionsOptic
is a mapped optic focused on every attraction from every national park.
ts
attractionsOptic .get (); // ['angels landing', 'great white throne', 'half dome', 'el capitan', 'mariposa grove', 'old faithful', 'grand prismatic']attractionsOptic .set (capitalize );attractionsOptic .get (); // ['Angels Landing', 'Great White Throne', 'Half Dome', 'El Capitan', 'Mariposa Grove', 'Old Faithful', 'Grand Prismatic']
ts
attractionsOptic .get (); // ['angels landing', 'great white throne', 'half dome', 'el capitan', 'mariposa grove', 'old faithful', 'grand prismatic']attractionsOptic .set (capitalize );attractionsOptic .get (); // ['Angels Landing', 'Great White Throne', 'Half Dome', 'El Capitan', 'Mariposa Grove', 'Old Faithful', 'Grand Prismatic']
Calling .get()
returns a flat representation of all attractions, even though in the root state they are nested in their respective national parks.
Updating them with .set()
preserves the state's original structure.
We can see that if we look at our state, now that we've applied all these transformations thanks to mapped optics:
ts
// nationalParksOptic.get();[{name: "Zion National Park",visitors: 6_039_835,attractions: ["Angels Landing", "Great White Throne"],},{name: "Yosemite National Park",visitors: 4_287_595,attractions: ["Half Dome", "El Capitan", "Mariposa Grove"],},{name: "Yellowstone National Park",visitors: 5_860_242,attractions: ["Old Faithful", "Grand Prismatic"],},];
ts
// nationalParksOptic.get();[{name: "Zion National Park",visitors: 6_039_835,attractions: ["Angels Landing", "Great White Throne"],},{name: "Yosemite National Park",visitors: 4_287_595,attractions: ["Half Dome", "El Capitan", "Mariposa Grove"],},{name: "Yellowstone National Park",visitors: 5_860_242,attractions: ["Old Faithful", "Grand Prismatic"],},];
- total: 1..1 default
- partial: 0..1
- mapped: 0..n
Derive all optics
The getOpticsFromMapping
function gets you an optic for each value focused by the mapped optic:
tsx
import {useOptic } from "@optics/react";constAttractions = () => {const [, {getOpticsFromMapping }] =useOptic (attractionsOptic );returngetOpticsFromMapping ((name ) =>name ).map (([key ,attractionOptic ]) => (<Attraction attractionOptic ={attractionOptic }key ={key } />));};
tsx
import {useOptic } from "@optics/react";constAttractions = () => {const [, {getOpticsFromMapping }] =useOptic (attractionsOptic );returngetOpticsFromMapping ((name ) =>name ).map (([key ,attractionOptic ]) => (<Attraction attractionOptic ={attractionOptic }key ={key } />));};
Reduce
Mapped optics have access to a method called reduce. It allows you to derive a new optic from the the values you're mapping over.
The function you pass to reduce
takes the array of values we're mapping over as its argument, and it returns either multiple of these values or a single one.
For example to focus on all attractions sorted by name...:
ts
constsortedAttractionsOptic =attractionsOptic .reduce ((attractions ) =>[...attractions ].sort ((a ,b ) =>a .value .localeCompare (b .value )));sortedAttractionsOptic .get (); // ['Angels Landing', 'El Capitan', 'Grand Prismatic', 'Great White Throne', 'Half Dome', 'Mariposa Grove', 'Old Faithful']
ts
constsortedAttractionsOptic =attractionsOptic .reduce ((attractions ) =>[...attractions ].sort ((a ,b ) =>a .value .localeCompare (b .value )));sortedAttractionsOptic .get (); // ['Angels Landing', 'El Capitan', 'Grand Prismatic', 'Great White Throne', 'Half Dome', 'Mariposa Grove', 'Old Faithful']
Each value in the array is wrapped in an object with a value
property, that's why we're using a.value
and b.value
in the sort function.
... and then to focus on the first three:
ts
constfirstThreeSortedOptic =sortedAttractionsOptic .reduce ((attractions ) =>attractions .slice (0, 3));firstThreeSortedOptic .get (); // ['Angels Landing', 'El Capitan', 'Grand Prismatic']firstThreeSortedOptic .set ((prev ) =>prev .toUpperCase ());attractionsOptic .get (); // ['ANGELS LANDING', 'Great White Throne', 'Half Dome', 'EL CAPITAN', 'Old Faithful', 'Mariposa Grove', 'GRAND PRISMATIC']
ts
constfirstThreeSortedOptic =sortedAttractionsOptic .reduce ((attractions ) =>attractions .slice (0, 3));firstThreeSortedOptic .get (); // ['Angels Landing', 'El Capitan', 'Grand Prismatic']firstThreeSortedOptic .set ((prev ) =>prev .toUpperCase ());attractionsOptic .get (); // ['ANGELS LANDING', 'Great White Throne', 'Half Dome', 'EL CAPITAN', 'Old Faithful', 'Mariposa Grove', 'GRAND PRISMATIC']
Back to one (or none)
The examples above return a mapped optic because we're returning multiple values, but you can also focus back on a single element simply by returning it.
For example to focus on the first attraction starting with the letter G:
ts
constfirstStartingWithGOptic =attractionsOptic .reduce ((attractions ) =>attractions .find ((attraction ) =>attraction .value .startsWith ("G")));firstStartingWithGOptic .get (); // 'Great White Throne'
ts
constfirstStartingWithGOptic =attractionsOptic .reduce ((attractions ) =>attractions .find ((attraction ) =>attraction .value .startsWith ("G")));firstStartingWithGOptic .get (); // 'Great White Throne'
Or to focus on the attraction with the shortest name:
ts
constshortestNameOptic =attractionsOptic .reduce ((attractions ) =>attractions .reduce ((acc ,cv ) =>cv .value .length <acc .value .length ?cv :acc ));shortestNameOptic .get (); // 'Half Dome'
ts
constshortestNameOptic =attractionsOptic .reduce ((attractions ) =>attractions .reduce ((acc ,cv ) =>cv .value .length <acc .value .length ?cv :acc ));shortestNameOptic .get (); // 'Half Dome'
We get a partial optic because the mapped optic might yield no value at all, in case the arrays we mapped over are empty.