Lenses in F#

I recently came across the concept of lenses in functional programming - a useful tool that can significantly reduce the amount of boilerplate code required when working with record types. I first noticed them in suave's source code and, intrigued, did some further research which lead me to this paper by Albert Steckermeier which describes the concept in detail (using Haskell).

As my background is primarily with OO languages I find that I quite often stumble across concepts in functional programming that more experienced practitioners would take for granted. There did not seem to a huge amount of information on lenses out there, so I thought I would write a short post about lenses in F#.

What is a lens?

A lens is simply a tuple of two functions, get and put, for a given part of a data structure. Using Steckermeier's terminology the data structure is the source and the part focused on is the view.

Using this terminology we can define a lens quite simply in F#.

type Lens<'S, 'V> = ('S -> 'V) * ('V -> 'S -> 'S)

We can trivially create a Lens module to allow us to elegantly call the get and put functions of a lens.

[<RequireQualifiedAccess>]
module Lens = 
    let get = fst
    let put = snd

As lenses are just tuples we can easily select the get or put function using the standard fst and snd functions.

Below is an example of a type with an associated lens and how that lens would be used.

type Address = {
  City : String;
  PostCode : String;
}
with

  //A lens for the PostCode field. The trailing underscore is a naming convention
  //used to denote lenses.
  static member PostCode_ = 
    (fun address -> address.PostCode),
    (fun postCode address -> { address with PostCode = postCode; })

let original = { City = "Cityville"; PostCode = "CV1"; }

//Get the post code using the lens
let postCode = Lens.get Address.PostCode_ address

//Update the post code using the lens
let updated = Lens.put Address.PostCode_ "CV3" address

Here we define a type Address (source) with an associated lens for the PostCode field (view). We then use the lens to get the value of the post code and to create an updated address with a different post code.

Composing lenses

The real power of lenses is that they are composable - you can combine two or more lenses to create a new lens which allows you to focus in on a part of the source that is normally only accessible by drilling down into the data structure - e.g. person.Address.PostCode.

Composing two lenses is simply a case of creating a new lens by composing the two sets of get and put functions. In order to do this the type of the view of the first lens must be the same as the type of the source of the second lens, i.e. composition has the signature Lens<'A, 'B> -> Lens<'B, 'C> -> Lens<'A, 'C>.

[<RequireQualifiedAccess>]
module Lens = 

    let compose (xLens : Lens<_,_>) (yLens : Lens<_,_>) = 

        let xGet, xPut = xLens
        let yGet, yPut = yLens

        let composedGet = xGet >> yGet

        let composedPut yView xSource =         

            let yUpdatedSource = yPut yView (xGet xSource)
            let xUpdatedSource = xPut yUpdatedSource xSource

            xUpdatedSource

        (composedGet, composedPut)

[<AutoOpen>]
module Operators = 

    let (>=>) = Lens.compose

For the sake of clarity I have made this implementation quite verbose, it can be written far more succintly. The infix operator >=> is included as a useful shorthand for composing lenses.

We can now demonstrate why lenses are so useful. Consider the following record types.

type Address = {
    City : String;
    PostCode : String;
}

type Person = {
    Name : String;
    Address : Address;
}

Now let's imagine that we have a Person record and we want to change the post code of the address.

let person' = { person with Address = { person.Address with PostCode = postCode; }; }

That's quite ugly, and it only gets worse as the data structures become more complicated. We can use lenses to neaten this code and reduce the amount of boilerplate code required. Assume that we have lenses Address.PostCode_ and Person.Address_.

let person' = Lens.put (Person.Address_ >=> Address.PostCode_) postCode person

This ability to compose lenses becomes increasingly useful the more complex the data structure becomes. They can be used inline, as above, or to create new functions.

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module Person = 

    //String -> Person -> Person
    let setPostCode = Lens.put (Person.Address_ >=> Address.PostCode_)

Finally, another useful aspect of lenses is that they are not restricted to working on single values. For example, let's expand our Address type and use a lens to work with multiple fields simultaneously.

type Address = {
    HouseNumber : Int32;
    StreetName : String;
    City : String;
    PostCode : String
}
with

    static member Street_ = 
        (fun address -> address.HouseNumber, address.StreetName),
        (fun (houseNumber, streetName) address -> 
            { address with HouseNumber = houseNumber; StreetName = streetName; })

let address = { HouseNumber = 1; StreetName = "Main Street"; City = "Cityville"; PostCode = "CV1"; }
let person = { Name = "Bob"; Address = address; }

let person' = Lens.put (Person.Address_ >=> Address.Street_) (23, "High Street") person

Conclusion

In this post I've given a quick overview of lenses and their implementation in F# and shown how they can be used to make working with record types a little more elegant.

If you are interested in seeing a more complete implementation of lenses in F# then I would suggest having a look at Aether on GitHub.

Comments