Skip to main content

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
const stateOptic = 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
const stateOptic = 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
const nationalParksOptic = stateOptic.map();
const nationalParksOptic: Optic<{ name: string; visitors: number; attractions: string[]; }, mapped>
ts
const nationalParksOptic = stateOptic.map();
const nationalParksOptic: Optic<{ name: string; visitors: number; attractions: string[]; }, mapped>

From there we can get the name of every national park ...

ts
const namesOptic = nationalParksOptic.name;
const namesOptic: Optic<string, mapped>
 
namesOptic.get(); // ['Zion', 'Yosemite', 'Yellowstone']
ts
const namesOptic = nationalParksOptic.name;
const namesOptic: Optic<string, mapped>
 
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
const visitorsOptic = nationalParksOptic.visitors;
const visitorsOptic: Optic<number, mapped>
 
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
const visitorsOptic = nationalParksOptic.visitors;
const visitorsOptic: Optic<number, mapped>
 
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.

tip

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
const attractionsOptic = nationalParksOptic.attractions.map();
const attractionsOptic: Optic<string, mapped>
ts
const attractionsOptic = nationalParksOptic.attractions.map();
const attractionsOptic: Optic<string, mapped>

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"],
},
];
cardinality recap
  • 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";
 
const Attractions = () => {
const [, { getOpticsFromMapping }] = useOptic(attractionsOptic);
 
return getOpticsFromMapping((name) => name).map(([key, attractionOptic]) => (
<Attraction attractionOptic={attractionOptic} key={key} />
));
};
tsx
import { useOptic } from "@optics/react";
 
const Attractions = () => {
const [, { getOpticsFromMapping }] = useOptic(attractionsOptic);
 
return getOpticsFromMapping((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
const sortedAttractionsOptic = 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
const sortedAttractionsOptic = 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']
info

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
const firstThreeSortedOptic = 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
const firstThreeSortedOptic = 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
const firstStartingWithGOptic = attractionsOptic.reduce((attractions) =>
attractions.find((attraction) => attraction.value.startsWith("G"))
);
 
firstStartingWithGOptic.get(); // 'Great White Throne'
const firstStartingWithGOptic: Optic<string, Omit<mapped, "map"> & partial>
ts
const firstStartingWithGOptic = attractionsOptic.reduce((attractions) =>
attractions.find((attraction) => attraction.value.startsWith("G"))
);
 
firstStartingWithGOptic.get(); // 'Great White Throne'
const firstStartingWithGOptic: Optic<string, Omit<mapped, "map"> & partial>

Or to focus on the attraction with the shortest name:

ts
const shortestNameOptic = attractionsOptic.reduce((attractions) =>
attractions.reduce((acc, cv) =>
cv.value.length < acc.value.length ? cv : acc
)
);
 
shortestNameOptic.get(); // 'Half Dome'
const shortestNameOptic: Optic<string, Omit<mapped, "map"> & partial>
ts
const shortestNameOptic = attractionsOptic.reduce((attractions) =>
attractions.reduce((acc, cv) =>
cv.value.length < acc.value.length ? cv : acc
)
);
 
shortestNameOptic.get(); // 'Half Dome'
const shortestNameOptic: Optic<string, Omit<mapped, "map"> & partial>
info

We get a partial optic because the mapped optic might yield no value at all, in case the arrays we mapped over are empty.