Part 2 of the series. Refer to the Part 1 to understand the basics.

In Part 1 of this series we've dealt with the basics of lenses and some of their applications like composition, virtual properties and maintaining invariants. Actually composition is not an application but rather a property of the functional lenses. Today I will introduce you to the concept of isomorphic lenses.

Isomorphism is a pair of transformations between two categories with no data loss.

JSON .stringify({a : 1 }); JSON .parse( '{"a":1}' );

You can image that isomorphism is a pair of two functions. A function that converts and a function that recovers. There are infinite number of isomorphisms out there that you're using daily. For example simple compression (gzip), cryptography or string transformations into buffers. Now that we understand what isomorphism is, we can apply it to the context of lenses. Imagine if we had a lens that could look at the data through any kind of ismorphism. Let's give this lens a name: lensIso.

There are only two things that our lensIso needs to know; how to convert and how to recover. Let's demonstrate on simple example.

const lensJSON = lensIso( JSON .parse, JSON .stringify);

Yeah! And that's all to it. We have created an isomorphic lens called lensJSON that knows how to focus on JSON object serialized into JSON string.

So what are the practical applications of lensJSON? We will use my favorite functional library ramda, that contains utilities for lenses manipulation.

Reading through isos

Reading through lensJSON means converting the JSON string into the JSON object.

R.view(lensJSON, '{"a": 1}' );

Writing through isos

Writing through lensJSON means converting the JSON object into JSON string.

R. set (lensJSON, {a: 2 }, '{"a":1}' );

These applications are not that interesting. But it is still good to be aware of the fact that this works exactly like primitive lenses. The really interesting things starts to happen if you want to modify the value through the isomorphic lens. Let's say we want to add property b to our JSON string.

"Modifying" through isos

R.over(lensJSON, R.assoc( 'b' , 3 ), '{"a":1}' );

Yeah, that's what I call a black magic! We are adding property b into JSON string in a declarative way. Our iso is handling the conversions under the hood. We're not actually modifying the original JSON string, but rather creating a new version of it (hence the quotes around "modifying").

Reversed isos

Now imagine if we had a mechanism for reversing the behavior of lensJSON. Actually this is something not that hard to implement. We just have to switch the convert and recover functions. We will "implement" it as a property of our lensIso function.

lensIso. from = ...the actual implementation...

Now that we have the mechanism for reversing the isomorphism we can do the following:

R.over(lensIso. from (lensJSON), R.replace( '}' , ',"b":2}' ), { a: 1 });

We're adding a JSON string fragment to the JSON object. I know this is not something you would normally do, but it really demonstrates the full power of the isomorphic lenses.

ramda-adjunct

You may be asking now: "How do I implement this lensIso function?". Well you don't need to worry about that. We've already implemented the lensIso function for you in ramda-adjunct library. If you're interested in the actual implementation do it at your own risk. But it may really harm your mental health ;] You can also go through the original article from Twan van Laarhoven about isos and see what laws they have to satisfy.

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: