Functional Programming Optics in .NET

William Barbosa
Mar 1 · 9 min read

Time to pay a visit to the functional eye doctor

Photo by Evan Wise on Unsplash

🔬Intro

There are a bunch of tutorials that teach you what functional programming optics are on the internet. Most of them, however, will literally just say lenses are just ('a -> 'b) * ('b -> 'a -> 'a), so what??. While these are 100% correct, I’m sure some people would understand the concept better if it was tied to a more concrete example. This article will try to explain what lenses and prisms are in a bit more detail using not only more code but also code that is not full of single letter variables.

Before we start, a little disclaimer: Lenses solve a problem that is inherent to immutable types. This means that C# classes don’t suffer from it all the time like F# records do. For that reason, all examples throughout the article will be written in F#. I will, however, include a sample at the end for those who are not very familiar with F#, since the concept is universal and can be applied to any programming language.

🙈The problem

Immutable types (Records in F# or any sort of type that you made immutable on C#) offer us greater safety when dealing with data because one can assume that no one else will change the instance that we are manipulating. When one needs to mutate a field in an immutable type, they are expected to instead create a new instance of the type:

type Player = { name: string; age: int }
let will = { name = "Will"; age = 27 }
let willButOlder = { will with age = 28 }
printf "%s" willButOlder.name //Outputs Will
printf "%d" willButOlder.age //Outputs 28

When writing types that are trying to model more complex domains, you might end up nesting types. That is expected. The problem is that when you want to mutate those objects, you also need to nest the copying:

type Format = Standard | Modern | Brawl

type Deck = {
name: string
format: Format
}

type Player = {
name: string;
age: int;
deck: Deck
}

let deck = { name = "Tron"; format = Modern }
let will = { name = "Will"; age = 27; deck = deck }
let willWithOtherDeck = { will with deck = { deck with name = "Eldrazi Tron" } }
printf "%s" willWithOtherDeck.name //Outputs Will
printf "%s" willWithOtherDeck.deck.name //Outputs Eldrazi Tron

Since there’s no such thing as changing the value of a property when you’re dealing with immutable data structures, what you need to do when you update a type that is nested is recreate every single object in the chain. For objects with a single level of nesting that doesn’t look that bad, but things can get out of hand pretty fast if you go too deep and then you will end up with so much nested with that you will barely be able to keep up what’s going on!

type Format = Standard | Modern | Brawl

type Deck = {
name: string
format: Format
}

type Player = {
name: string;
age: int;
deck: Deck
}

type Game = { player: Player; }

let deck = { name = "Tron"; format = Modern }
let will = { name = "Will"; age = 27; deck = deck }
let game = { player = will; }
let gameWhereWillHasAnotherDeck =
{ game with player =
{ game.player with deck =
{ game.player.deck with name = "Eldrazi Tron"} } }
printf "%s" gameWhereWillHasAnotherDeck.player.deck.name //Outputs Eldrazi Tron

🔍Lenses to the recue

A Lens is any type that conforms to the signature ('a -> 'b) * ('b -> 'a -> 'a) . That is, a pair of functions where the first receives a value of type a and returns a value of type b (IOW a getter) and the second one receives a value of type b and one of type a and returns another value of type a (IOW an immutable setter). In the context of the example above, here’s what a lens to set the player of a game would look like:

let gamePlayerLens =
(fun g -> g.player), (fun p g -> { g with player = p })

Now, you might be wondering why explaining such a basic concept like getters and setters is worth an article full of fancy names like Functional Programming Optics. Yes, lenses are a pretty simple thing to understand: they allow you to focus on a specific part of a type, zooming in on it and ignoring the rest. What makes lenses useful, however, is the fact that they can be composed! This means that, in our example, if I have a lens to set the deck for a player and a lens to set player for a game, I can combine them to change the deck of a game!

let gamePlayerLens =
(fun g -> g.player), (fun p g -> { g with player = p })

let playerDeckLens =
(fun p -> p.deck), (fun d p -> { p with deck = d })

let compose (getOne, setOne) (getTwo, setTwo) =
(fun x -> x |> getOne |> getTwo), (fun y x -> setOne (getOne x |> setTwo y) x)

let gameDeckLens = compose gamePlayerLens playerDeckLens

The compose function I just wrote is fairly straightforward once you get through the funky syntax. It’s literally just piping both functions (which are pure, since our setter returns a different value rather than mutating the original). This means that you can combine your lenses and reuse them whenever you need to change an object instead of getting lost in with hell:

let gamePlayerLens =
(fun g -> g.player), (fun p g -> { g with player = p })

let playerDeckLens =
(fun p -> p.deck), (fun d p -> { p with deck = d })

let compose (getOne, setOne) (getTwo, setTwo) =
(fun x -> x |> getOne |> getTwo), (fun y x -> setOne (getOne x |> setTwo y) x)

let gameDeckLens = compose gamePlayerLens playerDeckLens

let set lens x y =
let setter = snd lens
setter y x

let deck = { name = "Tron"; format = Modern }
let will = { name = "Will"; age = 27; deck = deck }
let game = { player = will; }
let gameWhereWillHasAnotherDeck =
set gameDeckLens { game.player.deck with name = "Eldrazi Tron"} game

printf "%s" gameWhereWillHasAnotherDeck.player.deck.name //Outputs Eldrazi Tron

💎Prims

Another syntax hell we might get ourselves into is when our model has to represent optional data; that is, data that might or might not be present. In F#, the way we do so is by using the Option monad, but the syntax for getting an optional value inside nested types can get clumsy really fast:

type Format = Standard | Modern | Brawl

type Deck = {
//Decks can now not have a name
name: string option
format: Format
}

type Player = {
name: string;
// Players need not specify their age
age: int option;
// Players might not have a deck yet
deck: Deck option
}

type Game = {
// A game might not have a player yet (e.g.: The login screen)
player: Player option;
}

let deck = { name = Some "Tron"; format = Modern }
let will = { name = "Will"; age = Some 27; deck = Some deck }
let game = { player = Some will; }

let nameOfTheDeckOfTheCurrentGame =
match game.player with
| None -> None
| Some player ->
match player.deck with
| None -> None
| Some deck -> deck.name

//This prints "Tron"
nameOfTheDeckOfTheCurrentGame
|> Option.defaultValue ""
|> printf "%s"

Imagine having to do that much pattern matching whenever you need to get the value of that property! Lenses sadly can’t quite help here, since lenses require exact type matches and declaring our types as optional would break the possibility for composition. The solution for that is our second topic: Prisms!

Prisms are a concept similar to lenses, the only difference being that they support the Option monad. The signature of a Prism is ('a -> 'b option) * ('b -> 'a -> 'a) , that is just a lens where the getter returns a ‘b option.

When you combine a lens with a prism, the result is a prism, since if anything is the chain does not have a value, that means the whole expression should return None . The code above would be massively simplified if we combined prisms; it’d be as trivial as passing a Game to a function and getting an Deck option back from it!

🚧Libraries

As you can probably tell, the code for using lenses is fairly straight forward to write. There are, however, many well tested and battle-hardened libraries out there; there’s no reason to reinvent the wheel in this case!

One library that does exactly one thing (Optics) is Aether. It provides a bunch of infix operators that make combining lenses a breeze! You can check this file here to see what the combine functions look like to wrap your head around the concepts.

In case you already need some other utilities and want a more robust library that does more than just optics, FSharpPlus has a lens module. It offers many different ways of composing lenses, getter functions and etc. There’s no silver bullet here and no library is better than other, so make sure to check the syntax and what each library offers before making a decision!

👴What about my C#

Here’s a very simple example of what an implementation of Lens would look like in C#, following the same example we modeled in F#. Note that I’m constraining the Lens type to only work with structs so that I can benefit from Nullable<T> , which also constrains to struct. The example below is really verbose because C# has a lot more noise than F#, but could be improved by using Records and nullable reference types.

using System;

public delegate B Getter<A, B>(A objectToFocus);
public delegate A Setter<A, B>(A objectToFocus, B valueToSet);

public struct Lens<A, B>
where A : struct
where B : struct
{
public Getter<A, B> Get { get; }

public Setter<A, B> Set { get; }

public Lens(Getter<A, B> get, Setter<A, B> set)
{
Get = get;
Set = set;
}
}

public struct Prism<T, U>
where T : struct
where U : struct
{
public Getter<T, U?> Get { get; }

public Setter<T, U> Set { get; }

public Prism(Getter<T, U?> get, Setter<T, U> set)
{
Get = get;
Set = set;
}
}

public struct Deck
{
public int NumberOfCards { get; }

public Deck(int numberOfCards)
{
NumberOfCards = numberOfCards;
}

public Deck With(int? numberOfCards = null)
=> new Deck(numberOfCards ?? NumberOfCards);
}

public struct Player
{
public Deck Deck { get; }

public int Age { get; }

public Player(Deck deck, int age)
{
Deck = deck;
Age = age;
}

public Player With(Deck? deck = null, int? number = null)
=> new Player(deck ?? Deck, number ?? Age);
}

public struct Game
{
public Player Player { get; }

public int GamesPlayed { get; }

public Game(Player player, int gamesPlayed)
{
Player = player;
GamesPlayed = gamesPlayed;
}

public Game With(Player? player = null, int? gamesPlayed = null)
=> new Game(player ?? Player, gamesPlayed ?? GamesPlayed);
}

public static class OpticsExtensions
{
public static Lens<A, C> Compose<A, B, C>(this Lens<A, B> lens, Lens<B, C> otherLens)
where A : struct
where B : struct
where C : struct
=> new Lens<A, C>(a => otherLens.Get(lens.Get(a)), (a, c) => lens.Set(a, otherLens.Set(lens.Get(a), c)));
}

public static class Showcase
{
static Showcase()
{
var gamePlayerLens = new Lens<Game, Player>(g => g.Player, (g, p) => g.With(p));
var playerDeckLens = new Lens<Player, Deck>(p => p.Deck, (p, d) => p.With(d));
var deckNumberOfCards = new Lens<Deck, int>(d => d.NumberOfCards, (d, n) => d.With(n));

var gameNumberOfCardsLens = gamePlayerLens.Compose(playerDeckLens).Compose(deckNumberOfCards);

var deck = new Deck(numberOfCards: 60);
var player = new Player(deck, age: 27);
var game = new Game(player, gamesPlayed: 0);
//Prints 1
Console.WriteLine(gameNumberOfCardsLens.Get(game));

var withNewFoo = game.With(game.Player.With(game.Player.Deck.With(numberOfCards: 100)));
var composedNewFoo = gameNumberOfCardsLens.Set(game, 100);
//Prints 5
Console.WriteLine(gamePlayerLens.Get(composedNewFoo));
Console.WriteLine(composedNewFoo.Player.Deck.NumberOfCards);
}
}

The implementation of a Compose function for the Prism type is left as an exercise to the reader. For a more complete library that provides types to use optics (and a lot of other functional tools) check the language-ext repository.

That was all for today. I hope this was useful. If you have any doubts, feel free to reach out on twitter and happy coding!

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade