DEV Community

Cover image for Death of a lens(man)
O.F.K.
O.F.K.

Posted on

Death of a lens(man)

Focusing on the issue

Taking a break from my Elixir-to-F# implementation, I encountered an issue when dealing with some other code I had lying around, that made me think, and admire, how F#'s syntax evolved lately, in response to actual problems folks developing in it had - a real living language, that puts its users' needs front and center.

Imagine an F# domain that has a deeply nested record structure, e.g. record-of-record-of-record..., not too far removed from a JSON of JSONs, for example:

// Indeed, this domain is as contrived as they get, and excessively sub-optimal... a perfect example!
type PersonName = {FirstName: string; LastName: string}
(*
Not related to the post's topic, but `HouseNumber` is a `string`! **Never** use numeric types (e.g., int, float, decimal) to denote quantities that can't be acted upon mathematically, even if they are **represented** as numbers. Numeric types should only be assigned to numeric quantities! A valid exception is `Id`, and even then, in production, prefer using `System.Guid`
*)
type Locale = {Street: string; HouseNumber: string}
type Address = {City: string; Locale: Locale}
type Details = {Name: PersonName; Address: Address}
type Employee = {Id: int; Details: Details}

let sherlockHolmes = {
    Id = 1
    Details = {
        Name = {
            FirstName = "Sherlock"
            LastName = "Holmes"
        }
        Address = {
            City = "London"
            Locale = {
                Street = "Baker"
                HouseNumber = "221B"
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, suppose the good detective decided to rent a more cost-effective apartment on the opposite side of the street, making his new address 222 Baker street.

How would we represent this in our data structure?

Seeing what the problem is

An interesting question to ask is "why is that a problem in the first place? Just update the nested member and be done with it!"... which is a great suggestion, really, no cynicism implied, except that in F# that used to be not exactly straightforward. To put it mildly.

The thing is records in F#, like all F#-native data structures, are immutable.

That means a couple of things:

  • When "updating" the dear detective, we will not be updating the current memory object that holds the data, instead we'd be creating a new data structure that is a clone of the current one, with any of our updates applied to said new data structure.
  • There is no way to tell the F# compiler "take this object and update the HouseNumber member, that is three-levels deep nested, leaving the rest of the data intact". This operation, easily done in C# with a single line of code, albeit a long-one, it is three-levels deep, would take us in F# considerable effort.

Eyeing the solution

In the "classic" F# syntax, prior to version 8, and of course, still valid today for the "classist" amongst us, the way to do that would have been:

let sherlockHolmesUpdated = {
    sherlockHolmes with
        Details = {
            sherlockHolmes.Details with
                Address = {
                    sherlockHolmes.Details.Address with
                        Locale = {
                            sherlockHolmes.Details.Address.Locale with
                                HouseNumber = "222"
                        }
                }
        }
}
Enter fullscreen mode Exit fullscreen mode

That is, using F#'s record update method we need to specify all the nested level up to and including the level we want to change. As the change is more deeply nested, the number of time we need to specify the entire chain grows, leading to a very accurate, true, but highly unreadable "pyramid of change", so to speak.

Surely, there is a better way of doing this?

Seeing with pinpoint accuracy

Well, the answer is "yes, of course". But also "not quite, no".

Which is a great time to introduce the subject of optics.

In Functional Programming languages, optics is a name given to a set of functions dealing with nested data structures. The tongue-in-cheek reasoning is that "optics let the user focus their code with pinpoint accuracy on the data of interest".

I will not be explaining optics in the post, but we need to know they exist, check, and that the first such pattern, the most used of them all is lenses, that allow getting, and setting, data in said highly-nested data structures. Check too.

The rub? Well, I said it... optics libraries need to be implemented before we can use them, they're not built-in into the language!

Indeed, how could they? There's no telling what are data structures would look like.

Even the existing libraries don't implement specific lenses, but rather give the user a framework to implement their own, according to their data.

I'm keeping the discussion on lenses, and optics in general, vague and hand-wavy on purpose: it's a huge one, complex, and while using them isn't too hard to grok, implementing them is worse than a root canal without anesthesia!

Still, for those interested: you can read, some more, and here too, for starter.

(For the very inquisitive minds, FSharpPlus has a very robust optics module, one such sub-module is Lens. You can look up the code on GitHub and see just how complex implementing a lens framework is.

Let's just say that applicatives is the least complex implementation detail!)

Rose-tinted glasses to all

So, updating deeply nested data is a chore in F#, whether using the classic approach, or using a lens library and implementing them for our specific data using the framework afforded by the library.

That was true, as I hinted earlier, till F#8 (for reference on November 2025 Microsoft will release F#10) when an update to the syntax of the most used case of the optics, the lens, was delivered.

Let's see just how easy it is to let dear Sherlock move out now:

let sherlockHolmesUpdated = {
    sherlockHolmes with
        Employee.Details.Address.Locale.HouseNumber = "222"
}
Enter fullscreen mode Exit fullscreen mode

Yup, that's it!

Using the "classic" record update syntax, instead of updating each nested level on its own, we now simply build a chain of the nested levels up to the change location and... that's it, presto magic!

The only caveat in this new syntax is that it must start at the top-most level of nesting: Sherlock is an Employee, so we must start our chain with it (some texts seem to omit this initial level, which will result in type error.)

Multiple changes in one go also behave the same:

let sherlockInLiverpool = {
    sherlockHolmes with
        Employee.Details.Address.City = "Liverpool"
        Employee.Details.Address.Locale.Street = "Something St."
        Employee.Details.Address.Locale.HouseNumber = "5"
    }
Enter fullscreen mode Exit fullscreen mode

And, for comparison with the classic method, that would be:

let sherlockInLiverpoolClassicApproach =
    { sherlockHolmes with
        Details =
            { sherlockHolmes.Details with
                Address =
                    { sherlockHolmes.Details.Address with
                        City = "Liverpool"
                        Locale =
                            { sherlockHolmes.Details.Address.Locale with
                                Street = "Something st."
                                HouseNumber = "5"
                            }
                    }
            }
    }
Enter fullscreen mode Exit fullscreen mode

Was blind but now can see

The new syntax turned out to be so good at cutting down the burden of updating records that several optics libraries maintainers stopped maintaining their projects.

F# now has imperative-style nested records update method, with clean, comprehensible syntax.

It's really fun when language maintainers, in the case of F# that would be Microsoft, listen to its users' qualms and issues and fix them (even if it takes some time.)

Which reminds me... eat F#'s shorts, Go! 🤪😂

Top comments (0)