Lenses

node v10.24.1
version: 2.0.1
endpointsharetweet
locations: lists employees by their location and position
const locations = { berlin: { employees: { staff: { list: [ { name: "Wiley Moen", phone: "688-031-5608", id: "cdfa-f2ae" }, { name: "Sydni Keebler", phone: "129-526-0289", id: "e0ec-e480" } ] }, managers: { list: [ { name: "Cecilia Wisoky", phone: "148-188-6725", id: "9ebf-5a73" } ] }, students: { list: [ { name: "Kirsten Denesik", phone: "938-634-9476", id: "c816-2234" } ] } } }, paris: { employees: { staff: { list: [ { name: "Lucius Herman", phone: "264-660-0107", id: "c2fc-55da" } ] }, managers: { list: [ { name: "Miss Rickie Smith", phone: "734-742-5829", id: "2095-69a7" } ] } } } };
3 different types of `compose`:
// recursive version const compose = (...fns) => x => fns.length ? compose(...fns.slice(0, -1))( fns[fns.length - 1](x) ) : x; // iterative version const composeItr = (...fns) => x => { const functions = Array.from( fns ).reverse(); /* `reverse` mutates the array, so we make a shallow copy of the functions array */ let result = x; for (const f of functions) { result = f(result); } return result; }; // with Array.prototype.reduce const composeReduce = (...fns) => x => fns.reduceRight( (result, f) => f(result), x ); // use it! console.log( compose( x => `Hello ${x}`, x => `${x}!` )("World") ); // -> "Hello World!"
Composed getters
const studentsAtLocation = compose( (students = {}) => students.list || [], (employees = {}) => employees.students, (location = {}) => location.employees ); const locationWithName = locationName => ( locations = {} ) => locations[locationName]; const getBerlinStudents = compose( studentsAtLocation, locationWithName("berlin") ); const getParisStudents = compose( studentsAtLocation, locationWithName("paris") ); console.log( getBerlinStudents(locations) ); // [ { name: 'Kirsten Denesik', ... ] console.log( getParisStudents(locations) ); // []
Function to create a composable getter and setter pair:
Explanation: 1. After calling `createComposableGetterSetter` with a getter and a setter function as arguments, we get back the actutal `composableGetterSetter`. 2. Our `composableGetterSetter` will get a `toGetterAndSetter` function, that takes some data as input and returns an object with a `get` and a `set` method. We return a function, that expects the target data as its only argument. 3. We construct a GetterSetter object by calling **(1)** with the target data from **(2)** and passing the return value to the `toGetterAndSetter` function. 4. We use the GetterSetter objects `set()` method with the return value of calling the setter **(4)** with the value of the constructed GetterSetter object (we call `getterSetter.get()` to simply retrieve this value) and the targetData (we expect, that the setter will return a new version of `targetData` with its focused value set to the return value from `getterSetter.get()`). 5. We return the value (which is again a GetterSetter object) that is returned from `getterSetter.set(...)` in **(5)**.
const createComposableGetterSetter = ( getter, // (1) // -- getter(targetData: TargetData): Value setter // (4) // -- setter(value: Value, targetData: TargetData) => TargetData ) => toGetterAndSetter => targetData => { // (2) const getterSetter = toGetterAndSetter( getter(targetData) ); // (3) /** * toGetterAndSetter is called with * "data" as argument * and returns a GetterSetter object: * @typedef { * { * get: function(): *, * set: function(newData: *): GetterSetter * } * } GetterSetter * */ return getterSetter.set( setter( getterSetter.get(), targetData ) ); // (5) };
Used for setting values with a composable getter and setter pair:
const toSetAccessors = data => ({ get: () => data, set: newData => toSetAccessors(newData) });
Used for getting values with a composable getter and setter pair:
const toGetAccessors = data => ({ get: () => data, set() { return this; } });
Creating Lenses
First draft:
let parisLens = createComposableGetterSetter( (obj = {}) => obj.paris, (value, obj) => ({ ...obj, paris: value }) ); let employeesLens = createComposableGetterSetter( (obj = {}) => obj.employees, (value, obj) => ({ ...obj, employees: value }) ); let studentsLens = createComposableGetterSetter( (obj = {}) => obj.students, (value, obj) => ({ ...obj, students: value }) );
After refactoring:
const lensProp = propName => createComposableGetterSetter( (obj = {}) => obj[propName], (value, obj) => ({ ...obj, [propName]: value }) ); // we can now create lenses for props like this: parisLens = lensProp("paris"); employeesLens = lensProp( "employees" ); studentsLens = lensProp( "students" ); const listLens = lensProp("list"); // needed to get the list of students
Composing (and using) lenses:
const parisStudentListLens = compose( parisLens, employeesLens, studentsLens, listLens ); const parisStudentList = parisStudentListLens( toGetAccessors )(locations).get(); console.log(parisStudentList); // -> undefined, since there is no list of students for paris defined. const locationsWithStudentListForParis = parisStudentListLens( _list => toSetAccessors([]) // ignore current list and replace it with an empty array )(locations).get(); console.log( locationsWithStudentListForParis ); // -> { ..., paris: { employees:{ ..., students: { list: [] } } } }
Helpers: `view`, `set`, `over`:
const view = (lens, targetData) => lens(toGetAccessors)( targetData ).get(); const over = ( lens, overFn /* like the `mapf` callback in `Array.prototype.map(mapf)`. i.e.: You get a value and return a new value. */, targetData ) => lens(data => toSetAccessors(overFn(data)) )(targetData).get(); const set = (lens, value, targetData) => over( lens, () => value /* we use `over` with a `overFn` function, that just returns the value argument */, targetData );
Using `get`, `set` and `over`:
{ const locationsWithStudentListForParis = set( parisStudentListLens, [], locations ); const locationsWithOneStudentInParis = over( parisStudentListLens, (list = []) => [ ...list, { name: "You", setVia: "Lens" } ], locations ); const locationsWithTwoStudentInParis = over( parisStudentListLens, (list = []) => [ ...list, { name: "Me", setVia: "Lens" } ], locationsWithOneStudentInParis ); // logging the results: console.log( view(parisStudentListLens, locations) ); // -> undefined console.log( view( parisStudentListLens, locationsWithStudentListForParis ) ); // -> [] console.log( view( parisStudentListLens, locationsWithTwoStudentInParis ) ); // -> [ { name: 'You', setVia: 'Lens' }, { name: 'Me', setVia: 'Lens' } ] console.log( view( parisStudentListLens, locationsWithOneStudentInParis ) ); // -> [ { name: 'Me', setVia: 'Lens' } ] console.log( locationsWithTwoStudentInParis ); // -> ... }
Improved version of `createComposableGetterSetter`: `createLens`
const createLens = (getter, setter) => toFunctor => targetData => toFunctor(getter(targetData)).map(focus => setter(focus, targetData)); // redifine `toGetAccessors` and `toSetAccessors` // toGetAccessors const Const = data => ({ get value() { return data; }, map() { return this; } }); // toSetAccessors const Identity = data => ({ get value() { return data; }, map(mapf) { return Identity(mapf(data)); } }); // redefine view, set, over const helpers = { view: (lens, targetData) => lens(Const)(targetData).value, over: ( lens, overFn /* like the `mapf` callback in `Array.prototype.map(mapf)`. i.e.: You get a value and return a new value. */, targetData ) => lens(data => Identity(overFn(data)))(targetData).value, set: (lens, value, targetData) => helpers.over( lens, () => value /* we use `over` with a `overFn` function, that just returns the value argument */, targetData ) }; // lens creator helpers: const lensCreators = { lensProp: propName => createLens( (obj = {}) => obj[propName], (value, obj) => ({ ...obj, [propName]: value }) ), lensIndex: (index = 0) => createLens( (arr = []) => arr[index], (value, arr = []) => arr.slice(0, index).concat(value, arr.slice(index)) ), lensPath: (path = []) => compose( ...path.reduce( (lenses, nextPathElem) => [ ...lenses, typeof nextPathElem === "number" ? lensCreators.lensIndex(nextPathElem) : lensCreators.lensProp(nextPathElem) ], [] ) ) }; { const { lensPath, lensProp } = lensCreators; const { view, over, set } = helpers; const studentsList = lensPath(["employees", "students", "list"]); const berlinStudents = compose( lensProp("berlin"), studentsList ); const parisStudents = compose( lensProp("paris"), studentsList ); const locationsWithStudentListForParis = set(parisStudents, [], locations); const locationsWithOneStudentInParis = over( parisStudents, (list = []) => [...list, { name: "You", setVia: "Lens" }], locations ); const locationsWithTwoStudentsInBerlin = over( berlinStudents, (list = []) => [...list, { name: "Me", setVia: "Lens" }], locationsWithOneStudentInParis ); console.log( "Paris:", "before:", view(parisStudents, locations) // undefined ); console.log( "after:", view(parisStudents, locationsWithOneStudentInParis) // [{name: "You", setVia: "Lens"}] ); console.log( "Berlin:", "before:", view(berlinStudents, locationsWithOneStudentInParis) // [{id: "c816-2234", name: "Kirsten Denesik", …}] ); console.log( "after:", view( berlinStudents, locationsWithTwoStudentsInBerlin ) /* [ {id: "c816-2234", name: "Kirsten Denesik", …}, {name: "Me", setVia: "Lens"} ] */, locationsWithTwoStudentsInBerlin ); }
Loading…

no comments

    sign in to comment