Do We Need Lenses for Records?

Immubitably

Do We Need Lenses for Records?

C#9 Records are here and I'm extremely excited. Records are a wonderful addition for those of us who are looking for more support for programming with immutable data types. Similar to Scala's case class, Kotlin's data class, and F#'s record, C#'s records get value semantics for free and great interop with other features of the language like destructuring and pattern-matching.

Although the initial reaction to records seems positive, working with immutable data structures comes with some pain points. I'm concerned that as people start working with records they'll come to the conclusion that they are too clumsy to work with. Discouraging their use without investigating how other communities have addressed theses issues.

Handling Nested Data

One of my biggest struggles when I first started working with immutable data structures was working with nested data. More specifically, updating a deeply nested structure.

For example, lets say we have a record called Point:

public record Point(int X, int Y);

Then a another record called NamedPoint that is composed of a Point and a string for the name:

public record NamedPoint(Point Point, string Name);

What happens when you want to update the X value of an instance of NamedPoint? If you were using a regular C# class, you might try myNamedPoint.Point.X = 100 to update the X to 100. Doing it this way comes with all the regular perils of mutability, but its easy.

You might also use inheritance to handle your nesting instead of composition. Doing so might tame the constructor a little. But if you're like me, you prefer composition over inheritance. And the question remains, how do you update X in NestedPoint?

C# has a nice with syntax, similar to F#'s, for making new records derived from old ones:

var oldPoint = new Point(100, 200);
var oldNamedPoint = new Point(oldPoint, "neat point");

var newNamedPoint = 
	oldNamedPoint with { 
    	Point = oldNamedPoint.Point with { 
        	X = 1000 
        } 
    };
	

Honestly, I think this syntax is pretty nice on its own, but any more nesting and it would get a little too unwieldy for my liking. This is where I worry C# developers will leave. It will be too cumbersome to deal with nesting, and they'll revert to traditional structures.

Optics

One way of addressing this issue that I've started learning about recently is Optics. Lenses specifically in the case of records. Lenses provide a composable way to access and "update" immutable data structures.

Each Lens contains a function that acts as a "getter" and another that acts as a  "setter". One way to implement a lens in C#9 might be with a record that contains a Get function and a Set function:

public record Lens<A, B>(Func<A, B> Get, Func<B, A, A> Set);

An instance of our Lens for Point that gets and sets the X might look like this:

var pointX =
	new Lens<Point, int>(
		Get: (point) => point.X,
		Set: (i, point) => point with { X = i });

And an instance of Lens for NamedPoint that gets and sets the Point:

var namedPointPoint =
	new Lens<NamedPoint, Point>(
		Get: (namedPoint) => namedPoint.Point,
		Set: (point, namedPoint) => namedPoint with { Point = point });

It looks like a lot of work just to get and set fields in a record, and it is. But remember I said that lenses are composable. They can be composed together to create new lenses that get and set nested data.

First, lets implement a Compose method for Lens. Records support methods, so we could add Compose as a method on Lens<A, B> itself, or as a standalone static method/extension method. I'm going to use the extension method route here.

    public static class LensExtensions {

        public static Lens<A, C> Compose<A, B, C>(
        	this Lens<A, B> @this, 
            Lens<B, C> other) =>
            new Lens<A, C>(
                Get: (a) => other.Get(@this.Get(a)),
                Set: (c, a) => @this.Set(other.Set(c, @this.Get(a)), a));
    }

Now we can make a new lens instance that is a composition of our pointX and namedPointPoint lenses:

var namedPointX = namedPointPoint.Compose(pointX);

Using namedPointX, all it takes to get the X of a NamedPoint is namedPointX.Get(aNamedPoint). Not a huge upgrade over aNamedPoint.Point.X (if you consider it an upgrade at all), but not bad. All that is required to set X, however, is

var aNewNamedPoint = namedPointX.Set(1000, aNamedPoint);

which I'd argue is quite the upgrade over

var aNewNamedPoint = 
	aNamedPoint with { 
    	Point = aNamedPoint.Point with { 
        	X = 1000 
        } 
    };

especially when you consider that multiple lenses can be composed together just as easily for a data structure with even deeper nesting.

Do we need them?

I love functional programming and I love what lenses and their sibling "Prisms" offer. But are they the right abstraction for C#?

I wish I had a good answer for this question, but I really don't know. I love each new feature that pushes C# deeper into the territory of functional programming. But with each new feature that I love, it's one more that everyone has to learn. It can take a real toll on developers.

Scala has a reputation for having at least two wildly different camps in its community: one that just wants "a better Java" and another that embraces Haskell like functional programming for the JVM. Take this with a grain of salt, I'm certainly no Scala insider. It's only what I've heard. I find Scala beautiful and love what I've seen of its capabilities, but others think its blend of functional and object-oriented features is too complex.

Is it possible that as we add more and more functional features and libraries to C# we will fracture the community into two separate camps like Scala's? If so, are we ok with that? Or would those of us who like functional programming be better off encouraging the use of F# for functional programming? Focusing more on interop between F# and C# rather than functional features for C#?

Ease of use

As we saw earlier, making an instance of our Lens is somewhat verbose, but the logic is not complicated. To justify their use, it would be nice if we could have the lens instance code generated for us. It would certainly make adoption easier.

I'm hoping the new source generators will come in handy here. I believe Scala's Monocle library is able to use macros for generating some of the boilerplate. Since we don't have macros in C#, it would be great if source generators could perform that function in our ecosystem!

To Infinity

As C# accrues features, we shouldn't fear looking outside of our bubble to get ideas for how to use them, even if we decide those ideas do not create the right abstractions for us.

Reference: Aether, The Lens Pattern in TypeScript, Basic Lensing, Lenses, C# Source Generators, Optics Beyond Lenses with Monocle, A (very short) into to optics, Monocle