DracTec.Optics 0.0.3

dotnet add package DracTec.Optics --version 0.0.3
                    
NuGet\Install-Package DracTec.Optics -Version 0.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DracTec.Optics" Version="0.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DracTec.Optics" Version="0.0.3" />
                    
Directory.Packages.props
<PackageReference Include="DracTec.Optics" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DracTec.Optics --version 0.0.3
                    
#r "nuget: DracTec.Optics, 0.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#addin nuget:?package=DracTec.Optics&version=0.0.3
                    
Install DracTec.Optics as a Cake Addin
#tool nuget:?package=DracTec.Optics&version=0.0.3
                    
Install DracTec.Optics as a Cake Tool

Fast Optics

This blog post provides a great introduction about why optics are useful.

There are some implementations for Lenses in C# out there (dadhi/Lens.cs, Tinkoff/Visor, ...) but all of these implementations incur a significant runtime overhead. And I like to have my cake and eat it too. Additionally, I wanted useful and simple and easy-to-understand lenses without all the fancy FP terms that can be used by junior developers as well as seasoned developers.

The goal of this project is to provide the convenience of functional optics without any significant runtime overhead.

Still not sure why you'd want this? Look at this (deprived) benchmark example:

[Benchmark]
public Base SetXWithLenses()
{
    return Base.Lens.NamedPoint.Point.Pos.X.Set(_base, 13);
}

[Benchmark]
public Base SetXRegularly()
{
    return _base with
    {
        NamedPoint = _base.NamedPoint with
        {
            Point = _base.NamedPoint.Point with { Pos = _base.NamedPoint.Point.Pos with { X = 13 } }
        }
    };
}

Getting Started

You can install DracTec.Optics with NuGet:

Install-Package DracTec.Optics

Or via the .NET Core command line interface:

dotnet add package DracTec.Optics

Either commands, from Package Manager Console or .NET Core CLI, will download and install DracTec.Optics.

Usage

Add the [WithLenses] attribute to your top level record that you want lenses for:

public record struct Name(string First, string Last);

[WithLenses]
public partial record Person(Name Name, int Age);

Now you can use the generated lenses with effectively zero overhead!

Person alice = new Person(new Name("Alice", "Smith"), 23);
Person marriedAlice = Person.Lens.Name.Last.Set(alice, "Thorsson");
Debug.Assert(marriedAlice.Name.Last == Person.Lens.Name.Last.Get(alice));

Alternatively, you can generate lenses for records that you do not own. In both cases, the generated lens is a singleton that is only allocated once.

[Lens(".Name.First")]
public static partial ILens<Person, string> FirstNameLens { get; }

[Lens(".Name.Last")]
public static partial ILens<Person, string> LastNameLens();

Lenses can also be reused and composed (with some overhead)

[WithLenses]
public record struct Name(string First, string Last);

[WithLenses(isRecursive: false)]
public partial record Person(Name Name, int Age);

ILens<Person, string> firstName = Person.Lens.Name.Combine(Name.Lens.First);
Person richard = new Person(new("Richard", "Smith"), 42);
Person rick = firstName.Set(richard, "Rick");

// happy birthday!
Person olderRick = Person.Lens.Age.Update(rick, age => age + 1);

List<Person> people = queryAllPeople();
var firstNames = people.Select(firstName.AsFunc).ToList();

Benchmark Results


BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4890/23H2/2023Update/SunValley3)
12th Gen Intel Core i5-12600K, 1 CPU, 16 logical and 10 physical cores
.NET SDK 9.0.101
  [Host]     : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
  DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2


Method Mean Error StdDev Median
SetXWithLenses 18.4337 ns 0.4108 ns 0.9684 ns 18.2913 ns
SetXWithLensesThroughInterface 17.5305 ns 0.3938 ns 0.8727 ns 17.3956 ns
SetXRegularly 16.9095 ns 0.3647 ns 0.9735 ns 16.5907 ns
GetXWithLenses 0.0129 ns 0.0057 ns 0.0050 ns 0.0146 ns
GetXWithLensesThroughInterface 0.1277 ns 0.0038 ns 0.0032 ns 0.1269 ns
GetXRegularly 0.1283 ns 0.0037 ns 0.0033 ns 0.1288 ns

To note: GetXWithLenses uses the generated Get method directly, which has a [MethodImpl(MethodImplOptions.AggressiveInlining)]. This seems to generate better results than just accessing the properties directly in this benchmark case.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.0.3 96 3/1/2025
0.0.2 106 2/24/2025
0.0.0-preview.0.6 57 2/24/2025
0.0.0-preview.0 59 2/24/2025