Creating Lenses in Elixir

This is the first article on my own medium site, so welcome! The articles I write here will be for my own learnings/discussions and will be a bit rougher but I hope people find them useful.

In an effort to better my functional programming and Elixir understanding I thought I’d discuss lenses.

There are lots of articles about how to use a lens but very few on how we would implement them. Today, we’ll implement our own lens while discussing the hows and whys.

This article assumes a basic understanding of what a lens is but let’s still mention…

What is a Lens

Think of a lens like a magnifying glass. It helps you retrieve data from a nested data structure, and make precise changes within it. All the while maintaining the goodness we know in functional purity and immutability.

A lens is not too dissimilar to getters and setters in imperative languages with 4 primary functions that we use when working with our data structures:
view: The view function is used to retrieve data from our nested structure.
over: over is a higher order function. It requires a function which takes the data from the record your lens is pointing at, and returns the entire structure with the update applied.
set: Set is similar to over but instead of taking a function it takes a constant value. The field that the lens is pointing at will be updated with this constant value.
concat/compose/append: A single purpose lens isn’t that useful so you will typically find a concat/compose/append function in the library that lets you join multiple lenses together. This is how you traverse deep within a structure with a lens.

Why use a Lens?

Firstly, you get a single representation of your data structure that you’re using. With a lens you create it once then can get and set the data within a record. You can pass it between functions, compose multiple lenses together, create a view that let’s you work with the data you will receive, etc.

Another example is when you wish to defer code execution until the last possible moment when you need the data. As we’ll see shortly, a lens basically boils down to function composition. You are creating a function chain that will be executed only when you provide the data you’re interested in.

Finally, if you are working in a typed language which can infer types a lens provides the functionality to change a record’s type. In Elixir that isn’t much of a problem but I feel it’s worth mentioning.

There are others but in the interest of brevity let’s just dive into an example.

Example Lens Usage

First things first, let’s create a test file with a deeply nested state that we will work with.

So here we can see that we have various nesting examples that we can use through the rest of our tests.

Let’s first confirm that the view function will, actually, view a record:

Here we have created, and extended, a lens that will reach right into the deepest point in the tree. This is a similar example to using get_in([:nested, :deeply_nested, :tally])

So now that we have a state and proven that we can view deeply, let’s set deeply with a constant value.

This would be similar to put_in(%{ a: %{ b: %{ c: “abc” } } }, [:a, :b, :c], 6)

Now let’s have a bit of fun with over. We’ll start with a function to increment a tally from the lens provided:

Lens.over takes a state, a function that will be applied to the lens record found, and a lens. We extend the lens received to point to the tally and update. This is similar to update_in(state, [:tally], &(&1 + 1))

So now let’s create the tests. I will just create 3 tests where each level of state will be updated, confirming that concat works with updating, we confirmed view and set above, asserting on the single nested lens and starting with an empty lens.

We could even avoid destructuring state and start with Lens.from(:state)

So that example doesn’t really show much of a difference from using update_in but it highlights an example of concatenating lenses to go deeply. I’ll create one more example. A pattern I think might be nice when working through deeply nested structures, one where each function in a chain will update the state and pass forward or simply pass their extended lens to their nested functions.

So we have our tests and examples of simple lens usage. Now, let’s actually create our own lens.

Implement the Lens

Lens’ are simply a tuple of composed functions. One to set and one to get. There are various ways to implement a lens with higher order functions but we’ll keep it simple with tuples.

In our tests we are working exclusively with objects so let’s create the 2 functions that get and set values in a Map.

You could also create a Lens.fromIdx that will can with lists, and any other assortment. You just need to be able to retrieve from the structure, and set it. Let’s stay with Maps for now.

So that’s pretty straight forward. We provide a key and get/put in the Map. So how does the Lens.view look?

Super easy, just use the get function on the record provided. I’ve created 2 versions of the view function, so we can use pipelines to build lenses or records.

Finally we have concat. Now as we are only working with functions, concat will be the composition of the functions.

As we’re working with lenses I’ll comment here that get is a contravariant function and set is covariant. Contravariant virtually boils down to “more explicitly defined through the result” while covariant is a function that returns a less specific type. This is the main reason I went on my lens journey, to discover what is a profunctor. We could push right out to a profunctor but that’s wayyyy too deep for this article so let’s move on.

So the new get function calls the left lens first, and then the right lens. This is the process of becoming more explicit in our result, the contravariant function implementation.

Set is a bit more convoluted as we only apply the change to the internal structure but we need to update the external to maintain immutability. Here we need to get the outer lens’ focus point (l_get.(obj)), pass that to the inner lens and make the changes. We then update the outer object with the inner result and then we return to our full record. This is a more typical function composition where we evaluate right to left.

So this gives us the ability to view deeply nested structures and a function to extend our lenses. Let’s create our set and over functions:

So that’s it! Here we have our set and over functions, plus the inverse to simplify pipelines. We’re evaluating in our over and passing the result straight into set which will update our record.

That is the full implementation that we need! 🎉 If you look at the supported lenses, like focus, they have type matching, error handling, etc that you would expect from a library but this is the core of what a common lens is, and how they are implemented.

VS get_in, put_in and update_in

So now we’re getting to the pointy end of the article. Elixir already has the helper functions of get_in and update_in so why would we use lenses? A single tuple that works with both updating and getting is appealing if we have a need to pass them around. A lens is therefore more explicit than a keyword list. It might not be enough to adopt an entirely new pattern though, or enough to move away from other state management patterns. Just a tool for the toolbox perhaps?

However, as this is our lens we can build upon this base setup with the other Lens concepts like Prisms (which handles error cases when fields are not present, focus has a great implementation) or extend and modify the lens itself, following Functor map. Let’s do that.

Map over our Lens

Above we have the test which transforms our Map and adds insert_prop to the larger map when it returns. It would be nice if we could always have that translation baked into our lens, wherever we pass it, without having the additional evaluation or creating a variable with the custom lens!

I’m going to leverage the map function name, as it’s the functor function. Enum.map and Stream.map execute a function on each item they contain and returns the result within the Enum or Stream type. These make them Functors. So let’s execute a function on our data within our lens as we work with it and return another lens. Test first:

This is the exact same result as the previous nested test except we don’t need to capture the lens we’re working with. We are running the function over the :nested Map record and then passing through to the deeply_nested function.

So how would we implement this? With get being used every time we call set and over we only need to change get.

So here we have run the function over whatever get returns and return the change. We are also maintaining contravariance on the get function so we will always know what it’s doing! Same with concat.

Conclusion

After learning, using and writing my own lens libraries I’m not entirely sure where I would use them within Elixir. At least in the apps that I work with, primarily APIs so state is minimal and pipelines manage data transformation really well. I do like that it is a popular pattern though and used in other functional languages. Elm/Purescript interest me for front end development and have very deeply nested states so a lens may make sense.

I do, however, like the pattern in React with simple state. I like redux but that is a bit heavy at times when all I want to do is work with a root component’s state. That’s not Elixir and that’s why you’re here so … signing off!

  • Aaron

Feel free to follow me on twitter @aabrook. I don’t post often but am always happy to read/share/chat about Elixir or any functional dev in general