lodash-lens

functional lens to use together with lodash library

Usage no npm install needed!

<script type="module">
  import lodashLens from 'https://cdn.skypack.dev/lodash-lens';
</script>

README

lodash-lens

minimalistic functional lens to use together with lodash library

links:

Motivation

Functional Lenses is a great concept which helps keep code clean and concise.

Using lens allows to manupulate object internals in an immutable way.

There are full-fledged implementions like Ramda or partial.lenses. They are great libraries but it's power is sometimes too much for small projects. From the other side, using lodash/fp normally has a minimal overhead. Unfortunately, the lodash/fp doesn't have functional lenses :(

This library implements the Lens concept in a quite mimimalistic way and is designed to work on the top of excellent lodash / lodash/fp stack with miminum overhead.

Installation

> npm install --save-dev lodash-lens

Usage

lens builder

To declare lens we need to define a getter and setter functions:

declare function lens(
  getter: (obj:any)=>any,
  setter: (obj:any, val:any)=>any
): LensFunc;

import { get, set } from 'lodash/fp';
import { lens } from 'lodash-lens';

const pathLens = lens(get(path), set(path));

lens operators

Using lens operators it is possible to manupulate object internals in an immutable way

view

view allows to get access to object highligted by lens:

test('should get nested object', () => {
  const obj = {a:{b:{c:1}}};
  const result = view(pathLens('a.b'), obj);
  expect(result).toBe(obj.a.b);
});

over

over allows to apply some function to object highlighted by lens

test('should over nested path', () => {
  const obj = {a:{b:1}};
  const result = over(pathLens('a.b'), (val=>val+1), obj);
  expect(result).toEqual({a:{b:2}});
  expect(result !== obj).toBeTruthy();
});

replace

replace allows to replace to object highlighted by lens to new one

test('should replace for nested path', () => {
  const obj = {a:{b:1}};
  const result = replace(pathLens('a.b'), 2, obj);
  expect(result).toEqual({a:{b:2}});
  expect(result !== obj).toBeTruthy();
});

merge

merge allows to merge an object highlighted by lens and a new one:

test('should shallow only objects in path', () => {
  const obj = {a:{b:{c:2}}, d:3};
  const result = merge(pathLens('a.b'), {e:4}, obj);
  expect(result).toEqual({a:{b:{c:2, e:4}}, d:3});
  expect(result !== obj).toBeTruthy();
  expect(result.a === obj.a).toBeFalsy();
  expect(result.a.b === obj.a.b).toBeFalsy();
  expect(result.a.c === obj.a.c).toBeTruthy();
  expect(result.d === obj.d).toBeTruthy();
});

Note that access operators do not make a deep clone of the source object but create a shallow copy of all objects in access path and keeps all other references untouched. That kind of work allows to perform this operation in a memory-effective way keeping immutability invariant.

operators currying

All lens operators have an auto-currying over all arguments:

test('should over nested path', () => {
  const overAB = over(pathLens('a.b'), (val=>val+1));
  
  const obj = {a:{b:1}};
  const result = overAB(obj);
  expect(result).toEqual({a:{b:2}});
  expect(result !== obj).toBeTruthy();
});  

predefined lenses

pathLens

pathLens highlight object by lodash get/set accessors

test('should view nested level', () => {
  const obj = {a:{b:{c:1}}};
  const result = view(pathLens(['a','b']), obj);
  expect(result).toBe(obj.a.b);
});
test('should over nested level', () => {
  const obj = {a:[{b:1}]};
  const result = over(pathLens(['a', 0, 'b']), (val=>val+1), obj);
  expect(result !== obj).toBeTruthy();
  expect(result).toEqual({a:[{b:2}]});
});

pickLens

pickLens give access to subset of object by keys

test('should view keys for array arg', () => {
  const obj = {a:1,b:2,c:3};
  const result = view(
    pickLens(['a','b']),
    obj);
  expect(result).toEqual({a:1,b:2});
});
import { mapValues } from 'lodash/fp';

test('should over keys', () => {
  const obj = {a:1,b:2,c:3};
  const result = over(
    pickLens(['a','b']),
    mapValues(val=>val+1),
    obj);
  expect(result).toEqual({a:2,b:3,c:3});
});

findLens

findLens give access to array members which can be located by standard lodash find function:

test('should over for identity search', () => {
  const obj = [{id:0, b:0},{id:1, b:1}];
  const result = over(
    findLens({id:1}),
    val=>({...val,b:2}), obj);
  expect(result).toEqual([{id:0, b:0},{id:1, b:2}]);
});

lens composition

lenses by itself are pure functions, so they can be easy composed:

import { compose } from 'lodash/fp';

test('should over for identity search', () => {
  const obj = {a:[{id:0, b:0},{id:1, b:1}]};
  const result = over(
    compose(pathLens('a'), findLens({id:1}), pathLens('b')),
    (val=>val+1), obj);
  expect(result).toEqual({a:[{id:0, b:0},{id:1, b:2}]});
});