Functional lenses are relatively new and very complex subject making them challenging to learn for novice functional programmers. The basic idea is that lens is a first class citizen (Function) that gives you access (focus) to the particular piece of the complex data structure. The access consists mainly from Read, Write and Modify operations. There are more advanced operations called Isos , Folds and Traversals but we will be dealing with them in the next article of this series.

Lenses compose. I wrote about this subject in my previous article about lenses composition. Composition if one of the main killer feature and allows you to do real magic. In the following demonstrations I will use JavaScript implementation of van Laarhoven polymorphic function references (or lenses) from my favorite functional library called ramda. Who the hell is van Laarhoven ? Well Twan van Laarhoven is a very clever person that come up with an idea of using Functors to create a representation for lenses. I will not dive in to the subject of lens implementation details. It is really really complicated stuff and took me while to absorb it. If you want to know how lenses really work under the hood, how the Functors are used to represent them I recommend starting with the following video.

Lenses applications

Basic demonstration

Let's start by basic demonstration of the power of lenses. Reading and modifying data structure with lenses.

const { lensProp, lens, view, set , prop, assoc, over, add } = require ( 'ramda' ); const dataStructure = { prop1: 1 , prop2: 'a' }; const prop1 = lensProp( '﻿prop1' ); const prop2 = lens(prop( 'prop2' ), assoc( 'prop2' )); view(prop1, dataStructure); set (prop1, 2 , dataStructure); over(prop1, add( 2 ), dataStructure); ﻿

Notice the definition of prop2. We are creating a lens and teaching it to focus on a prop2 attribute of the dataStructure. Definitions of prop1 and prop2 lenses are equivalent and lensProp(<prop>) is just a shortcut for lens(prop(<prop>), assoc(<prop>)).

Composition

Lenses are just function. So we compose them like ordinary functions. Lenses composition should always start with the most specific and should end with the less specific. I recommend using compose from ramda and compose them in order: less specific to the left, most specific to the right. compose then runs your lenses from right to left so everything works as expected. Writing lenses composition in this way feels more natural and is easier to read and reason about.

const { lensProp, view, set , over, compose, add } = require ( 'ramda' ); const dataStructure = { foo: { bar: 1 } }; const foo = lensProp( 'foo' ); const bar = lensProp( 'bar' ); const fooBar = compose(foo, bar); view(fooBar, dataStructure); set (fooBar, 2 , dataStructure); over(fooBar, add( 2 ), dataStructure);

Virtual properties

Lenses allows us to do operations on virtual properties of the data structure. We can have a data structure and a lens that gets a property from that data structure. But the property isn't really there. It's purely virtual and computed from the other property of the data structure.

const { lensProp, compose, add, lens, view, set, over } = require ( 'ramda' ); const dataStructure = { fahrenheit: -58 }; const far2cel = far => (far - 32 ) * ( 5 / 9 ); const cel2far = cel => (cel * 9 / 5 ) + 32 ; const fahrenheit = lensProp( 'fahrenheit' ); const lcelsius = lens(far2cel, cel2far); const celsius = compose(fahrenheit, lcelsius); view(celsius, dataStructure); set(celsius, -30 , dataStructure); over(celsius, add( 10 ), dataStructure);

I know what you think now. "It's beautiful". When I first wrote this code I could feel the teardrop pushing out of my eye ;] Let's break it down. We have two endomorphic functions: far2cel and cel2far that know how to transform Celsius (°C) to Fahrenheit (°F) and vice versa. Then we define the fahrenheit lens that knows how to pull fahrenheit property out of data structure. Next we create lcelsius lens and teach it how to transform values between the two scales. Finally we create celsius lens as a composition of fahrenheit and lcelsius lenses. And that's it. Now if we read the data structure through celsius lens, we firstly pull the the fahrenheit property out of data structure and run it through the far2cel before returning. Writing/modifying the data structure through the celsius lens means running the new value through cel2far and assigning the computed value back to the fahrenheit property.

Maintaining invariants

Suppose we have a data structure that stores hours and minutes. We want to have a lens that can read the minutes, but when adding/subtracting the minutes, the operation would also affect the hours.

const { lensProp, always, pipe, flip, subtract, add, lens, view, set , over } = require( 'ramda' ); const dataStructure = { hours: 2 , minutes: 58 }; const hours = lensProp( 'hours' ); const minutes = lensProp( 'minutes' ); const minutesInvariant = lens( view(minutes), ( value , target) => { if ( value > 60 ) { return pipe(over(hours, add( 1 )), set (minutes, value - 60 ))(target); } else if ( value < 0 ) { return pipe(over(hours, flip(subtract( 1 ))), set (minutes, value + 60 ))(target); } return over(minutes, always( value ), target); } ); view(minutesInvariant, dataStructure); set (minutesInvariant, 62 , dataStructure); over(minutesInvariant, add( 59 ), dataStructure);

Of course my invariant algorithm isn't perfect and is handling only limited set of use-cases but it is enough for the demonstration. I have created two lenses that maps the data structure (hours, minutes). Then I've used these lenses for composing my minutesInvariant lens. Remember the lens is just a function that needs to be told how to read and modify the data structure. And that is basically what we're doing here.

ramda-adjunct extensions

During my engagement with lenses I came up with another application of lenses. Using them as a predicates or focusing the data structure with the default value. This is the list of utils I came up with:

lensEq - Returns true if data structure focused by the given lens equals provided value

lensNotEq - Returns true if data structure focused by the given lens doesn't equal provided value

lensSatisfies - Returns true if data structure focused by the given lens satisfies the predicate. Note that the predicate is expected to return boolean value and will be evaluated as false unless the predicate returns true

lensNotSatisfy - Returns true if data structure focused by the given lens doesn't satisfy the predicate. Note that the predicate is expected to return boolean value

viewOr - Returns a "view" of the given data structure, determined by the given lens. The lens's focus determines which portion of the data structure is visible. Returns the defaultValue if "view" is null, undefined or NaN; otherwise the "view" is returned

All of this utils are implemented as part of ramda-adjunct now thanks to Michael Kuk who ported my old pull request on ramda-lens libary back to ramda-adjunct.

Conclusion

There are many more applications of lenses like Isos , Folds and Traversals . We'll deal with them next time. I hope this article helped you to understand the concept of functional lenses and some of their applications.

It is wondrous how the pure functions can be used even to create data focusers that do really complicated stuff. Like always I end my article with the usual axiom: Define your code-base as pure functions and lift them only if and when needed. In this case into the lens context. And compose, compose, compose...

Functional Lenses in JavaScript series: