I'm developing a GUI application, heavily working with graphics – you can think about it as a vector editor, for the sake of the example. It is very tempting to make all data structures immutable – so I could get undo/redo, copy/paste, and many other things almost without effort.
Fot the sake of simplicity, I will use the following example – application is used to edit polygonal shapes, so I have "Polygon" object, which is simply list of immutable points:
Scene -> Polygon -> Point
And so I have only one mutable variable in my program – the one which holds current Scene object. The problem which I have starts when I try to implement point dragging – in mutable version, I simply grab a Point
object and start modifying its coordinates. In immutable version – I am stuck. I could have stored indices of Polygon
in current Scene
, index of dragged point in Polygon
, and replace it every time. But this approach does not scale – when composition levels go to 5 and further, boilerplate would become unbearable.
I'm sure this problem can be solved – after all, there is Haskell with completely immutable structures and IO monad. But I just cannot find how.
Can you give me a hint?
Best Answer
You're absolutely right, this approach doesn't scale if you can't get around the boilerplate. Specifically, the boilerplate for creating a whole new Scene with a tiny subpart changed. However, many functional languages provide a construct for dealing with this sort of nested structure manipulation: lenses.
A lens is basically a getter and setter for immutable data. A lens has a focus on some small part of a larger structure. Given a lens, there are two things you can do with it - you can view the small part of a value of the larger structure, or you can set the small part of a value of a larger structure to a new value. For example, suppose you have a lens that focuses on the third item in a list:
That type means the larger structure is a list of things, and the small subpart is one of those things. Given this lens, you can view and set the third item in the list:
The reason lenses are useful is because they are values representing getters and setters, and you can abstract over them the same way you can other values. You can make functions that return lenses, for instance a
listItemLens
function which takes a numbern
and returns a lens viewing then
th item in a list. Additionally, lenses can be composed:Each lens encapsulates behavior for traversing one level of the data structure. By combining them, you can eliminate the boilerplate for traversing multiple levels of complex structures. For instance, supposing you have a
scenePolygonLens i
that views thei
th Polygon in a Scene, and apolygonPointLens n
that views thenth
Point in a Polygon, you can make a lens constructor for focusing on just the specific point you care about in an entire scene like so:Now suppose a user clicks point 3 of polygon 14 and moves it 10 pixels right. You can update your scene like so:
This nicely contains all the boilerplate for traversing and updating a Scene inside
lens
, all you have to care about is what you want to change the point to. You can further abstract this with alensTransform
function that accepts a lens, a target, and a function for updating the view of the target through the lens:This takes a function and turns it into an "updater" on a complicated data structure, applying the function to only the view and using it to construct a new view. So going back to the scenario of moving the 3rd point of the 14th polygon to the right 10 pixels, that can be expressed in terms of
lensTransform
like so:And that's all you need to update the whole scene. This is a very powerful idea and works very well when you have some nice functions for constructing lenses viewing the pieces of your data you care about.
However this is all pretty out-there stuff currently, even in the functional programming community. It's difficult to find good library support for working with lenses, and even more difficult to explain how they work and what the benefits are to your coworkers. Take this approach with a grain of salt.