Skip to content

Instantly share code, notes, and snippets.

@dadhi
Last active February 23, 2024 09:41
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dadhi/3db1ed45a60bceaa16d051ee9a4ab1b7 to your computer and use it in GitHub Desktop.
Save dadhi/3db1ed45a60bceaa16d051ee9a4ab1b7 to your computer and use it in GitHub Desktop.
Functional optics, e.g. Lens, in C# - to simplify access and modification for deep part of immutable value
/*
Inspired by: https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe
*/
using System;
using static System.Console;
public static class Program
{
public static void Main()
{
var x = new X("Lens");
var xs = x.Lens(
a => a.S, (_, s) => new X(s));
var lastChar = "".Lens(
a => a[a.Length - 1],
(a, b) => a.Substring(0, a.Length - 1) + b);
var xsLastChar = xs.Compose(lastChar);
WriteLine(xsLastChar.Set(x, 'Z').S);
WriteLine(xsLastChar.Update(x, char.ToUpper).S);
}
class X
{
public readonly string S;
public X(string s) { S = s; }
}
}
public struct Lens<A, B>
{
public Func<A, B> Get;
public Func<A, B, A> Set;
public Lens(Func<A, B> g, Func<A, B, A> s)
{ Get = g; Set = s; }
}
public static class LensExt
{
public static Lens<A, B> Lens<A, B>(
this A _, //used only for type inference
Func<A, B> g, Func<A, B, A> s) =>
new Lens<A, B>(g, s);
public static A Update<A, B>(
this Lens<A, B> lens,
A a, Func<B, B> update) =>
lens.Set(a, update(lens.Get(a)));
public static Lens<A, C> Compose<A, B, C>(
this Lens<A, B> ab, Lens<B, C> bc) =>
new Lens<A, C>(
a => bc.Get(ab.Get(a)),
(a, c) => ab.Set(a, bc.Set(ab.Get(a), c)));
}
@dzmitry-lahoda
Copy link

dzmitry-lahoda commented Jun 19, 2018

Enterprise version:

    /// <summary>
    /// Read and write immutable data (simplify access and modification for deep part of immutable value). 
    /// Should be used via <see cref="LensExtensions"/>.
    /// </summary>
    /// <remarks>
    /// Runtime overhead approach with delegate. 
    /// If you need zero runtime overhead probably should try do not use immutable at all 
    /// or implement OOP/Interface lenses with large code overhead.
    /// </remarks>
    /// <seealso href="https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe"/>
    /// <seealso href="https://github.com/dotnet/csharplang/issues/302"/>
    public readonly struct Lens<TWhole, TPart>
    {
        /// <summary>
        /// Gets value of whole from part.
        /// </summary>
        public readonly Func<TWhole, TPart> Get;

        /// <summary>
        /// Sets value of part and creates new whole with this.
        /// </summary>
        /// <remarks>
        /// Goes whole -> part -> whole, not part -> whole -> whole as we do not have partial application here.
        /// </remarks>
        public readonly Func<TWhole, TPart, TWhole> Set;

        /// <summary>
        /// Creates new object to read and write immutable object graphs.
        /// </summary>
        /// <param name="getter">Setter.</param>
        /// <param name="setter">Getter.</param>
        public Lens(Func<TWhole, TPart> getter, Func<TWhole, TPart, TWhole> setter)
        {
            Get = getter ?? throw new ArgumentNullException(nameof(getter));
            Set = setter ?? throw new ArgumentNullException(nameof(setter));
        }
    }

    /// <summary>
    /// Read and write immutable data (simplify access and modification for deep part of immutable value).
    /// </summary>
    public static class LensExtensions
    {
        /// <summary>
        /// Creates new lens.
        /// </summary>
        /// <typeparam name="TWhole">Type of whole.</typeparam>
        /// <typeparam name="TPart">Type of part inside whole.</typeparam>
        /// <param name="_">Used only for type inference. Ignore.</param>
        /// <param name="getter">Gets value of part from whole.</param>
        /// <param name="setter">Sets value of part in whole and creates new whole.</param>
        /// <returns>Object to read and write immutable object.</returns>
        public static Lens<TWhole, TPart> Lens<TWhole, TPart>(
            this TWhole _,
            Func<TWhole, TPart> getter, Func<TWhole, TPart, TWhole> setter) =>
            new Lens<TWhole, TPart>(getter, setter);

        /// <summary>
        /// Updates value in part.
        /// </summary>
        /// <typeparam name="TWhole">Type of whole.</typeparam>
        /// <typeparam name="TPart">Type of part.</typeparam>
        /// <param name="lens">Lens to use for update.</param>
        /// <param name="whole">Whole object.</param>
        /// <param name="update">Function to update part (create new updated).</param>
        /// <returns>Whole with updated value inside part.</returns>
        public static TWhole Update<TWhole, TPart>(
            this Lens<TWhole, TPart> lens,
            TWhole whole, Func<TPart, TPart> update) =>
            lens.Set(whole, update(lens.Get(whole)));

        /// <summary>
        /// Allows to access deep hierarchi from paretn to mid to child.
        /// </summary>
        /// <typeparam name="TWhole">Root parent. Whole.</typeparam>
        /// <typeparam name="TPart">Intermidate to access from parent to child. Part.</typeparam>
        /// <typeparam name="TSubPart">Deepest child. Sub part.</typeparam>
        /// <param name="parent">From parent lens.</param>
        /// <param name="child">To child lens.</param>
        /// <returns>Lens over three objects.</returns>
        public static Lens<TWhole, TSubPart> Compose<TWhole, TPart, TSubPart>(
            this Lens<TWhole, TPart> parent, Lens<TPart, TSubPart> child) =>
            new Lens<TWhole, TSubPart>(
              whole => child.Get(parent.Get(whole)),
              (whole, part) => parent.Set(whole, child.Set(parent.Get(whole), part)));
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment