avatarEric Elliott

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

6601

Abstract

w(lens, set(lens, a, store)) ≡ a</code> — If you set a value into the store, and immediately view the value through the lens, you get the value that was set.</li><li><code>set(lens, b, set(lens, a, store)) ≡ set(lens, b, store)</code> — If you set a lens value to <code>a</code> and then immediately set the lens value to <code>b</code>, it's the same as if you'd just set the value to <code>b</code>.</li><li><code>set(lens, view(lens, store), store) ≡ store</code> — If you get the lens value from the store, and then immediately set that value back into the store, the value is unchanged.</li></ol><p id="5385">Before we dive into code examples, remember that if you’re using lenses in production, you should probably be using a well tested lens library. The best one I know of in JavaScript is Ramda. We’re going to skip that for now and build some naive lenses ourselves, just for the sake of learning:</p><div id="1f58"><pre><span class="hljs-comment">// Pure functions to view and set which can be used with any lens:</span> <span class="hljs-keyword">const</span> view = (lens, store) => lens.view(store); <span class="hljs-keyword">const</span> <span class="hljs-keyword">set</span> = (lens, value, store) => lens.<span class="hljs-keyword">set</span>(value, store);</pre></div><div id="9b16"><pre><span class="hljs-comment">// A function which takes a prop, and returns naive</span> <span class="hljs-comment">// lens accessors for that prop.</span> const lensProp = <span class="hljs-function"><span class="hljs-params">prop</span> =></span> ({ <span class="hljs-attr">view</span>: <span class="hljs-function"><span class="hljs-params">store</span> =></span> store[prop], <span class="hljs-comment">// This is very naive, because it only works for objects:</span> set: <span class="hljs-function">(<span class="hljs-params">value, store</span>) =></span> ({ ...store, [prop]: value }) });</pre></div><div id="794c"><pre>// An example store <span class="hljs-selector-tag">object</span>. An <span class="hljs-selector-tag">object</span> you access with <span class="hljs-selector-tag">a</span> lens // is often called the "store" <span class="hljs-selector-tag">object</span>: const fooStore = { <span class="hljs-selector-tag">a</span>: <span class="hljs-string">'foo'</span>, b: <span class="hljs-string">'bar'</span> };</pre></div><div id="7e99"><pre><span class="hljs-attribute">const aLens</span> = lensProp(<span class="hljs-string">'a'</span>); <span class="hljs-attribute">const bLens</span> = lensProp(<span class="hljs-string">'b'</span>);</pre></div><div id="b392"><pre>// Destructure the a <span class="hljs-keyword">and</span> b props <span class="hljs-keyword">from</span> the lens <span class="hljs-keyword">using</span> // the <span class="hljs-keyword">view</span>() <span class="hljs-keyword">function</span>. const a = <span class="hljs-keyword">view</span>(aLens, fooStore); const b = <span class="hljs-keyword">view</span>(bLens, fooStore); console.log(a, b); // <span class="hljs-string">'foo'</span> <span class="hljs-string">'bar'</span></pre></div><div id="2d21"><pre><span class="hljs-comment">// Set a value into our store using the aLens:</span> <span class="hljs-keyword">const</span> bazStore = <span class="hljs-keyword">set</span>(aLens, <span class="hljs-string">'baz'</span>, fooStore);</pre></div><div id="8129"><pre><span class="hljs-comment">// View the newly set value.</span> <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>( view(aLens, bazStore) ); <span class="hljs-comment">// 'baz'</span></pre></div><p id="df67">Let’s prove the lens laws for these functions:</p><div id="7e95"><pre>const store <span class="hljs-operator">=</span> fooStore<span class="hljs-comment">;</span></pre></div><div id="4f8c"><pre>{ <span class="hljs-comment">// view(lens, set(lens, value, store)) = value</span> <span class="hljs-comment">// If you set a value into the store, and immediately</span> <span class="hljs-comment">// view the value through the lens, you get the value</span> <span class="hljs-comment">// that was set.</span> <span class="hljs-keyword">const</span> lens = lensProp(<span class="hljs-string">'a'</span>); <span class="hljs-keyword">const</span> value = <span class="hljs-string">'baz'</span>;</pre></div><div id="8bdf"><pre><span class="hljs-attribute"> const a</span> = value; <span class="hljs-attribute"> const b</span> = view(lens, set(lens, value, store));</pre></div><div id="5c22"><pre> <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(a, b); <span class="hljs-comment">// 'baz' 'baz'</span> }</pre></div><div id="5c52"><pre>{ <span class="hljs-comment">// set(lens, b, set(lens, a, store)) = set(lens, b, store)</span> <span class="hljs-comment">// If you set a lens value to a and then immediately set the lens value to b,</span> <span class="hljs-comment">// it's the same as if you'd just set the value to b.</span> const lens = lensProp('a');</pre></div><div id="9ac0"><pre><span class="hljs-attribute"> const a</span> = <span class="hljs-string">'bar'</span>; <span class="hljs-attribute"> const b</span> = <span class="hljs-string">'baz'</span>;</pre></div><div id="2407"><pre> <span class="hljs-keyword">const</span> r1 = <span class="hljs-built_in">set</span>(lens, b, <span class="hljs-built_in">set</span>(lens, a, store)); <span class="hljs-keyword">const</span> r2 = <span class="hljs-built_in">set</span>(lens, b, store);

console.<span class="hljs-keyword">log</span>(r1, r2); // {a: <span class="hljs-string">"baz"</span>, b: <span class="hljs-string">"bar"</span>} {a: <span class="hljs-string">"baz"</span>, b: <span class="hljs-string">"bar"</span>} }</pre></div><div id="54dd"><pre>{ <span class="hljs-comment">// set(lens, view(lens, store), store) = store</span> <span class="hljs-comment">// If you get the lens value from the store, and then immediately set that value</span> <span class="hljs-comment">// back into the store, the value is unchanged.</span> <span class="hljs-keyword">const</span> lens = lensProp(<span class="hljs-string">'a'</span>);</pre></div><div id="197b"><pre> <span class="hljs-keyword">const</span> r1 = <span class="hljs-built_in">set</span>(lens, view(lens, store), store); <span class="hljs-keyword">const</span> r2 = store;

console.<span class="hljs-keyword">log</span>(r1, r2); // {a: <span class="hljs-string">"foo"</span>, b: <span class="hljs-string">"bar"</span>} {a: <span class="hljs-string">"foo"</sp

Options

an>, b: <span class="hljs-string">"bar"</span>} }</pre></div><h1 id="8b71">Composing Lenses</h1><p id="504b">Lenses are composable. When you compose lenses, the resulting lens will dive deep into the object, traversing the full object path. Let’s import the more full-featured <code>lensProp</code> from Ramda to demonstrate:</p><div id="a40c"><pre><span class="hljs-keyword">import</span> { compose, lensProp, <span class="hljs-keyword">view</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">'ramda'</span>;</pre></div><div id="e299"><pre><span class="hljs-attribute">const lensProps</span> = [ <span class="hljs-string">'foo'</span>, <span class="hljs-string">'bar'</span>, 1 ];</pre></div><div id="ff7b"><pre>const lenses <span class="hljs-operator">=</span> lensProps.map(lensProp)<span class="hljs-comment">;</span> const truth <span class="hljs-operator">=</span> compose(...lenses)<span class="hljs-comment">;</span></pre></div><div id="7e45"><pre><span class="hljs-string">const</span> <span class="hljs-string">obj</span> <span class="hljs-string">=</span> { <span class="hljs-attr">foo:</span> { <span class="hljs-attr">bar:</span> [<span class="hljs-literal">false</span>, <span class="hljs-literal">true</span>] } }<span class="hljs-string">;</span></pre></div><div id="cf8c"><pre><span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>( view(truth, obj) );</pre></div><p id="0ad2">That’s great, but there’s more to composition with lenses that we should be aware of. Let’s take a deeper dive.</p><h1 id="6e5c">Over</h1><p id="6472">It’s possible to apply a function from <code>a => b</code> in the context of any functor data type. We've already demonstrated that functor mapping <i>is composition.</i> Similarly, we can apply a function to the value of focus in a lens. Typically, that value would be of the same type, so it would be a function from <code>a => a</code>. The lens map operation is commonly called "over" in JavaScript libraries. We can create it like this:</p><div id="27fc"><pre><span class="hljs-regexp">//</span> over = <span class="hljs-function"><span class="hljs-params">(lens, f: a => a, store)</span> =></span> store const over = <span class="hljs-function"><span class="hljs-params">(lens, f, store)</span> =></span> set(lens, f(view(lens, store)), store);</pre></div><div id="7789"><pre>const uppercase <span class="hljs-operator">=</span> x <span class="hljs-operator">=</span>> x.toUpperCase()<span class="hljs-comment">;</span></pre></div><div id="9177"><pre>console<span class="hljs-selector-class">.log</span>( over(aLens, uppercase, store) // { <span class="hljs-selector-tag">a</span>: <span class="hljs-string">"FOO"</span>, b: <span class="hljs-string">"bar"</span> } );</pre></div><p id="f406">Setters obey the functor laws:</p><div id="c2bc"><pre>{ <span class="hljs-comment">// if you map the identity function over a lens</span> <span class="hljs-comment">// the store is unchanged.</span> <span class="hljs-keyword">const</span> id = x => x; <span class="hljs-keyword">const</span> lens = aLens; <span class="hljs-keyword">const</span> a = over(lens, id, store); <span class="hljs-keyword">const</span> b = store;</pre></div><div id="f724"><pre> <span class="hljs-built_in">console</span>.<span class="hljs-built_in">log</span>(a, b); }</pre></div><p id="1a5f">For the composition example, we’re going to use an auto-curried version of over:</p><div id="4c83"><pre><span class="hljs-keyword">import</span> { curry } <span class="hljs-keyword">from</span> <span class="hljs-string">'ramda'</span>;</pre></div><div id="bf3c"><pre>const over = curry( (<span class="hljs-name">lens</span>, f, store) => set(<span class="hljs-name">lens</span>, f(<span class="hljs-name">view</span>(<span class="hljs-name">lens</span>, store)), store) )<span class="hljs-comment">;</span></pre></div><p id="f19f">Now it’s easy to see that lenses under the over operation also obey the functor composition law:</p><div id="4ba9"><pre>{ // <span class="hljs-keyword">over</span>(lens, f) <span class="hljs-keyword">after</span> <span class="hljs-keyword">over</span>(lens g) <span class="hljs-keyword">is</span> the same <span class="hljs-keyword">as</span> // <span class="hljs-keyword">over</span>(lens, compose(f, g)) const lens = aLens;</pre></div><div id="f163"><pre><span class="hljs-attribute"> const store</span> = { a: 20 };</pre></div><div id="08b7"><pre> const g = <span class="hljs-function"><span class="hljs-params">n</span> =></span> n + <span class="hljs-number">1</span>; const f = <span class="hljs-function"><span class="hljs-params">n</span> =></span> n * <span class="hljs-number">2</span>;</pre></div><div id="f5d9"><pre> const a = compose( <span class="hljs-name">over</span>(<span class="hljs-name">lens</span>, f), over(<span class="hljs-name">lens</span>, g) )<span class="hljs-comment">;</span></pre></div><div id="3483"><pre><span class="hljs-attribute"> const b</span> = over(lens, compose(f, g));</pre></div><div id="a94d"><pre> console<span class="hljs-selector-class">.log</span>( <span class="hljs-selector-tag">a</span>(store), // {<span class="hljs-selector-tag">a</span>: <span class="hljs-number">42</span>} <span class="hljs-selector-tag">b</span>(store) // {<span class="hljs-selector-tag">a</span>: <span class="hljs-number">42</span>} ); }</pre></div><p id="1443">We’ve barely scratched the surface of lenses here, but it should be enough to get you started. For a lot more, detail, Edward Kmett has spoken a lot on the topic, and many people have written much more in-depth explorations.</p><p id="ea07"><b><i>Eric Elliott</i></b><i> is a distributed systems expert and author of the books, <a href="https://leanpub.com/composingsoftware">“Composing Software”</a> and <a href="https://ericelliottjs.com/product/programming-javascript-applications-ebook/">“Programming JavaScript Applications”</a>. As co-founder of <a href="https://devanywhere.io/">DevAnywhere.io</a>, he teaches developers the skills they need to work remotely and embrace work/life balance. He builds and advises development teams for crypto projects, and has contributed to software experiences for <b>Adobe Systems,</b> <b>Zumba Fitness,</b> <b>The Wall Street Journal,</b> <b>ESPN,</b> <b>BBC,</b> and top recording artists including <b>Usher, Frank Ocean, Metallica,</b> and many more.</i></p><p id="0b51"><i>He enjoys a remote lifestyle with the most beautiful woman in the world.</i></p></article></body>

Lenses

Composable Getters and Setters for Functional Programming

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0)

Note: This is part of the “Composing Software” book that started life right here as a blog post series. It covers functional programming and compositional software techniques in JavaScript (ES6+) from the ground up. < Previous | << Start over at Part 1

A lens is a composable pair of pure getter and setter functions which focus on a particular field inside an object, and obey a set of axioms known as the lens laws. Think of the object as the whole and the field as the part. The getter takes a whole and returns the part of the object that the lens is focused on.

// view = whole => part

The setter takes a whole, and a value to set the part to, and returns a new whole with the part updated. Unlike a function which simply sets a value into an object’s member field, Lens setters are pure functions:

// set = whole => part => whole

Note: In this text, we’re going to use some naive lenses in the code examples just to give you a beneath-the-hood peek at the general concept. For production code, you should look at a well tested library like Ramda, instead. The API differs between different lens libraries, and it’s possible to express lenses in more composable, elegant ways than they are presented here.

Imagine you have a tuple array representing a point’s x, y, and z coordinates:

[x, y, z]

To get or set each field individually, you might create three lenses. One for each axis. You could manually create getters which focus on each field:

const getX = ([x]) => x;
const getY = ([x, y]) => y;
const getZ = ([x, y, z]) => z;
console.log(
  getZ([10, 10, 100]) // 100
);

Likewise, the corresponding setters might look like this:

const setY = ([x, _, z]) => y => ([x, y, z]);
console.log(
  setY([10, 10, 10])(999) // [10, 999, 10]
);

Why Lenses?

State shape dependencies are a common source of coupling in software. Many components may depend on the shape of some shared state, so if you need to later change the shape of that state, you have to change logic in multiple places.

Lenses allow you to abstract state shape behind getters and setters. Instead of littering your codebase with code that dives deep into the shape of a particular object, import a lens. If you later need to change the state shape, you can do so in the lens, and none of the code that depends on the lens will need to change.

This follows the principle that a small change in requirements should require only a small change in the system.

Background

In 1985, “Structure and Interpretation of Computer Programs” described getter and setter pairs (called put and get in the text) as a way to isolate an object's shape from the code that uses the object. The text shows how to create generic selectors that access parts of a complex number independent of how the number is represented. That isolation is useful because it breaks state shape dependencies. These getter/setter pairs were a bit like referenced queries which have existed in relational databases for decades.

Lenses took the concept further by making getter/setter pairs more generic and composable. They were popularized after Edward Kmett released the Lens library for Haskell. He was influenced by Jeremy Gibbons and Bruno C. d. S. Oliveira, who demonstrated that traversals express the iterator pattern, Luke Palmer’s “accessors”, Twan van Laarhoven, and Russell O’Connor.

Note: An easy mistake to make is to equate the modern notion of a functional lens with Anamorphisms, based on Erik Meijer, Maarten Fokkinga, and Ross Paterson’s “Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire” in 1991. “The term ‘lens’ in the functional reference sense refers to the fact that it looks at part of a whole. The term ‘lens’ in a recursion scheme sense refers to the fact that [( and )] syntactically look kind of like concave lenses. tl;dr They have nothing to do with one another." ~ Edward Kmett on Stack Overflow

Lens Laws

The lens laws are algebraic axioms which ensure that the lens is well behaved.

  1. view(lens, set(lens, a, store)) ≡ a — If you set a value into the store, and immediately view the value through the lens, you get the value that was set.
  2. set(lens, b, set(lens, a, store)) ≡ set(lens, b, store) — If you set a lens value to a and then immediately set the lens value to b, it's the same as if you'd just set the value to b.
  3. set(lens, view(lens, store), store) ≡ store — If you get the lens value from the store, and then immediately set that value back into the store, the value is unchanged.

Before we dive into code examples, remember that if you’re using lenses in production, you should probably be using a well tested lens library. The best one I know of in JavaScript is Ramda. We’re going to skip that for now and build some naive lenses ourselves, just for the sake of learning:

// Pure functions to view and set which can be used with any lens:
const view = (lens, store) => lens.view(store);
const set = (lens, value, store) => lens.set(value, store);
// A function which takes a prop, and returns naive
// lens accessors for that prop.
const lensProp = prop => ({
  view: store => store[prop],
  // This is very naive, because it only works for objects:
  set: (value, store) => ({
    ...store,
    [prop]: value
  })
});
// An example store object. An object you access with a lens
// is often called the "store" object:
const fooStore = {
  a: 'foo',
  b: 'bar'
};
const aLens = lensProp('a');
const bLens = lensProp('b');
// Destructure the `a` and `b` props from the lens using
// the `view()` function.
const a = view(aLens, fooStore);
const b = view(bLens, fooStore);
console.log(a, b); // 'foo' 'bar'
// Set a value into our store using the `aLens`:
const bazStore = set(aLens, 'baz', fooStore);
// View the newly set value.
console.log( view(aLens, bazStore) ); // 'baz'

Let’s prove the lens laws for these functions:

const store = fooStore;
{
  // `view(lens, set(lens, value, store))` = `value`
  // If you set a value into the store, and immediately
  // view the value through the lens, you get the value
  // that was set.
  const lens = lensProp('a');
  const value = 'baz';
  const a = value;
  const b = view(lens, set(lens, value, store));
  console.log(a, b); // 'baz' 'baz'
}
{
  // set(lens, b, set(lens, a, store)) = set(lens, b, store)
  // If you set a lens value to `a` and then immediately set the lens value to `b`,
  // it's the same as if you'd just set the value to `b`.
  const lens = lensProp('a');
  const a = 'bar';
  const b = 'baz';
  const r1 = set(lens, b, set(lens, a, store));
  const r2 = set(lens, b, store);
  
  console.log(r1, r2); // {a: "baz", b: "bar"} {a: "baz", b: "bar"}
}
{
  // `set(lens, view(lens, store), store)` = `store`
  // If you get the lens value from the store, and then immediately set that value
  // back into the store, the value is unchanged.
  const lens = lensProp('a');
  const r1 = set(lens, view(lens, store), store);
  const r2 = store;
  
  console.log(r1, r2); // {a: "foo", b: "bar"} {a: "foo", b: "bar"}
}

Composing Lenses

Lenses are composable. When you compose lenses, the resulting lens will dive deep into the object, traversing the full object path. Let’s import the more full-featured lensProp from Ramda to demonstrate:

import { compose, lensProp, view } from 'ramda';
const lensProps = [
  'foo',
  'bar',
  1
];
const lenses = lensProps.map(lensProp);
const truth = compose(...lenses);
const obj = {
  foo: {
    bar: [false, true]
  }
};
console.log(
  view(truth, obj)
);

That’s great, but there’s more to composition with lenses that we should be aware of. Let’s take a deeper dive.

Over

It’s possible to apply a function from a => b in the context of any functor data type. We've already demonstrated that functor mapping is composition. Similarly, we can apply a function to the value of focus in a lens. Typically, that value would be of the same type, so it would be a function from a => a. The lens map operation is commonly called "over" in JavaScript libraries. We can create it like this:

// over = (lens, f: a => a, store) => store
const over = (lens, f, store) => set(lens, f(view(lens, store)), store);
const uppercase = x => x.toUpperCase();
console.log(
  over(aLens, uppercase, store) // { a: "FOO", b: "bar" }
);

Setters obey the functor laws:

{ // if you map the identity function over a lens
  // the store is unchanged.
  const id = x => x;
  const lens = aLens;
  const a = over(lens, id, store);
  const b = store;
  console.log(a, b);
}

For the composition example, we’re going to use an auto-curried version of over:

import { curry } from 'ramda';
const over = curry(
  (lens, f, store) => set(lens, f(view(lens, store)), store)
);

Now it’s easy to see that lenses under the over operation also obey the functor composition law:

{ // over(lens, f) after over(lens g) is the same as
  // over(lens, compose(f, g))
  const lens = aLens;
  const store = {
    a: 20
  };
  const g = n => n + 1;
  const f = n => n * 2;
  const a = compose(
    over(lens, f),
    over(lens, g)
  );
  const b = over(lens, compose(f, g));
  console.log(
    a(store), // {a: 42}
    b(store)  // {a: 42}
  );
}

We’ve barely scratched the surface of lenses here, but it should be enough to get you started. For a lot more, detail, Edward Kmett has spoken a lot on the topic, and many people have written much more in-depth explorations.

Eric Elliott is a distributed systems expert and author of the books, “Composing Software” and “Programming JavaScript Applications”. As co-founder of DevAnywhere.io, he teaches developers the skills they need to work remotely and embrace work/life balance. He builds and advises development teams for crypto projects, and has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.

He enjoys a remote lifestyle with the most beautiful woman in the world.

JavaScript
Programming
Technology
Software Engineering
Functional Programming
Recommended from ReadMedium