9

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?

2
  • @Job - that is how it works right now, and it gives me many pains. So I am looking for alternative approaches - and immutability seems perfect for this application structure, at least before we add user interaction to it :)
    – Rogach
    Dec 7, 2011 at 3:36
  • @Rogach: Can you explain more about your boilerplate code?
    – rwong
    Dec 7, 2011 at 6:07

3 Answers 3

10

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.

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:

thirdItemLens :: Lens [a] a

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:

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

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 number n and returns a lens viewing the nth item in a list. Additionally, lenses can be composed:

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

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 the ith Polygon in a Scene, and a polygonPointLens n that views the nth 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:

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

Now suppose a user clicks point 3 of polygon 14 and moves it 10 pixels right. You can update your scene like so:

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

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 a lensTransform function that accepts a lens, a target, and a function for updating the view of the target through the lens:

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

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:

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

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.

1
  • Excellent explanation ! Now i get what lenses are ! Mar 22, 2016 at 14:24
13

I have worked on exactly the same problem (but only with 3 composition levels). The basic idea is to clone, then modify. In immutable programming style, the cloning and modification has to happen together, which becomes command object.

Note that in mutable programming style, cloning would have been necessary anyway:

  • To allow undo/redo
  • The display system may need to simultaneously display the "before edit" and "during edit" model, overlapped (as ghost lines), so that the user can see the changes.

In mutable programming style,

  • The existing structure is deep-cloned
  • The changes are made in the cloned copy
  • The display engine is told to render the old structure in ghost-lines, and the cloned/modified structure in color.

In immutable programming style,

  • Each user action that results in data modification is mapped to a sequence of "commands".
  • A command object encapsulates exactly what modification is to be applied, and a reference to the original structure.
    • In my case, my command object only remembers the point index that needs to be changed, and the new coordinates. (i.e. very lightweight, as I'm not strictly following immutable style.)
  • When a command object is executed, it creates a modified deep-copy of the structure, making the modification permanent in the new copy.
  • As user makes more edits, more command objects will be created.
1
  • 1
    Why make a deep copy of an immutable data structure? You just need to copy the "spine" of references from the modified object to the root and retain references to the remaining portions of teh original structure. Mar 22, 2016 at 17:56
3

Deeply-immutable objects have the advantage that deep-cloning something simply requires copying a reference. They have the disadvantage that making even a small change to a deeply-nested object requires constructing a new instance of every object within which it is nested. Mutable objects have the advantage that changing an object is easy--just do it--but deep-cloning an object requires constructing a new object which contains a deep clone of every nested object. Worse, if one wants to clone an object and make a change, clone that object, make another change, etc. then no matter how big or small the changes are one has to keep a copy of the entire hierarchy for every saved version of the object's state. Nasty.

An approach that might be worth considering would be to define an abstract "maybeMutable" type with mutable and deeply-immutable derivative types. All such types would feature an AsImmutable method; calling that method on a deeply-immutable instance of an object would simply return that instance. Calling it on a mutable instance would return a deeply-immutable instance whose properties were deeply-immutable snapshots of their equivalents in the original. Immutable types with mutable equivalents would sport an AsMutable method, which would construct a mutable instance whose properties matched those of the original.

Changing a nested object in a deeply-immutable object would require first replacing the outer immutable object with a mutable one, then replacing the property containing the thing to be changed with a mutable one, etc. but making repeated changes to the same aspect of the overall object would not require making any additional objects until such time as an attempt was made to call AsImmutable on a mutable object (which would leave the mutable objects mutable, but return immutable objects holding the same data).

As simple but significant optimizations, each mutable object could hold a cached reference to an object of its associated immutable type, and each immutable type should cache its GetHashCode value. When calling AsImmutable on a mutable object, before returning a new immutable object, check it would match the cached reference. If so, return the cached reference (abandoning the new immutable object). Otherwise update the cached reference to hold the new object and return that. If this is done, repeated calls to AsImmutable without any intervening mutations will yield the same object references. Even if one doesn't save the cost of constructing the new instances, one will avoid the memory cost of keeping them. Further, equality comparisons between the immutable objects may be greatly expedited if in most cases the items being compared are reference-equal or have different hash codes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.