Optics - Get/Set properties for immutable data

in #programming9 years ago (edited)

Introduction

Working with immutable data is becomming common, even in languages like JavaScript. The trouble is that if you have nested data things can get complex really fast, even if you are working in a Functional language like F#. Optics (Lenses, Prism, And Endo and Isomorphisms) take away all that pain by providing OO style Property semantics for immutable type structures. They hide the complexity for you.

This is a slight detor from my "Building a roguelike in F# from scratch" series (Introduction, Repo and Initial Types, First Problems, Level Queries). While working on the CRUD sorts of manipulations on that project I quickly found I was travelling a similar road I had in the past. Growing complex code that carried too much cognative workload. Now, as back then, the answer was Optics. A clean and beautiful composition for working with immutable state, I believe from the Haskell world originally.

I will give you a quick overview on what they and the problem they solve.

The Problem

Immutable state carries a load of benefits that are hard to pass up. Just think about all those insane bugs you have had to chase that end up being some code changing a value in an object when they shouldn't. The down side is that for more complex structures the manipulations get equally complex.

This is because you never actually update immutable state, you create a copy of the original with the changes applied. Nothing can change under you creating bugs like this. The payoff is that when you have nested data and you change a child object you also have to build a new parent slotting the new child into place.

So take the following simple struicture:


type Address
    {
        street: string 
        town: string
        region: string 
        country: string
    }

type Person 
    { 
        name: string
        age: int
        address: Address
    }

To update the address for a person you build a new address and then a new person. This is not as bad as it coupld be in F#, The "with" operator takes on that heavy lifting for you.

var newPerson = 
    {person with 
        address = {person.address with town = updatedTown}}

No problems so far, clean enough and easy to follow.

Start to throw collections into the mix, such as a Dictionary/Map, and things start to spiral out of control quickly. We now need a means of Adding, Updating and deleting elements from the collection.

In OO world that is simple, everything is mutable:

person.phoneNumbers[Home] = 5551234;

To cut to the chase the Optics version of this is very similar thanks to some operator overloaded magic. You can see that the OO style . and = have been prefixed with ^ but apart from that the feel is similar.

let newPerson = person ^. phoneNumbers_ ^. Map.value_ Home ^= 5551234;

The big difference is person remains untouched, the optics compositions provide the way to traverse the structure, update the value and then rebuild the parts required.

Aether

Initially I started down the track of building my own Optics library from scratch, which I have done for C# in the past. I stumbled on the wonderful Aether library from Xyncro Tech. This provided everything I needed and is well implemented so this is my new goto Optics library for F#, which saved a chunk of work.

You will find similar libraries in most languages now where immutable data is more common, even JavaScript for when you are using Immutable.js

Lenses

I will start with Lenses as they are the easiest to understand. A Lens is a simple construct, just two functions that you could write in your sleep. One function to get a value and the other to set it.

let age_ =
    (fun person -> person.age),
    (fun age person -> {person with age = age})

The Get operation is simple enough, A function from Person to Age. Given a person it gives you the age value. The Set operation is a little more interesting, but not much. A function that takes the new value for Age and then the Person to insert it into, the intro above should mean this is also easy enough to follow, even if you are not familiar with F#

Currently you would be correct in thinking this does not buy us too much, and you would be correct. What makes the effort of writing all the Lenses for you types is how they plug together. First we need a couple more Lenses:

let address_ =
    (fun person -> person.address), 
    (fun address person -> {person with address = address})

let town_ =
    (fun address -> address.town), 
    (fun town address -> {address with town = town})

We can create a new Lens for address.town in the following way, which results in a lens that given a person will give you the town they live in.

let addressTown_ = address_ >-> town_

// In action
let newPerson = Optic.set addressTown_ "Christchurch"  person 

// Or
let newPerson = Optic.set (address_ >-> town_) "Christchurch"  person 

The operator plugs the two Lenses together and returns a new Lens that is the combination of the two.

The ^. and ^= operators make this the cleaner looking

let newPerson = person ^. addressTown_ ^= "Christchurch" 

The other stuff

Prisms and the various morphisms are all variations on the same theme, they just solve different problems, such as Optional data (Nullable in OO languages)

The Aether library page has a good write up on the various use cases etc.

Hope this has sparked some interest in this fascinating function composition, feel free to ask if you have any questions

Happy coding

Woz

UPDATE: See Building a roguelike in F# from scratch - CRUD Operations for an updated example of this Optics library. This contains some mistakes and understanding around ^. and ^=

Coin Marketplace

STEEM 0.04
TRX 0.33
JST 0.095
BTC 62390.74
ETH 1741.56
USDT 1.00
SBD 0.39