Lenses in C#
I first read about lenses in my adventures with Haskell.
A lens is an accessor for getting/setting a field in a data structure.
There are good reasons for lenses to exist in a pure, functional programming environment like Haskell. I thought lenses was a neat idea and it would be useful to have them in C#. So I came up with the following definition:
class Lens<TObject, TProperty>
{
public Func<TObject, TProperty> Get {get; private set;}
public Action<TObject, TProperty> Set {get; private set;}
public Lens(Func<TObject, TProperty> getter, Action<TObject, TProperty> setter)
{
Get = getter;
Set = setter;
}
}
The Lens<> class is a combination of:
- a getter function – allowing us to get the value of a property.
- a setter function – allowing us to set the value of a property.
This is the essence of a lens. Lenses allow us to treat object properties as first-class values, so they can be stored in variables, and passed into and out of functions.
What about using the Lens<> class? It looks like this (LINQPad-friendly):
// a simple class for which we are going to create lenses for the properties
class Person
{
public string Name {get;set;}
public int Age {get;set;}
}
void Main()
{
// lens for the Name property
var name = new Lens<Person,string>(p => p.Name, (p, v) => p.Name = v);
// lens for the Age property
var age = new Lens<Person,int>(p => p.Age, (p, v) => p.Age = v);
var person = new Person {Name = "Peter", Age = 30};
// use the lenses to get the property values
name.Get(person).Dump(); // output: Peter
age.Get(person).Dump(); // output: 30
// use the lens to set the Name property value
name.Set(person, "Michael");
name.Get(person).Dump(); // output: Michael
}
This seems simple enough, but creating a lens for a property is more verbose than I would like it to be. I would just like to say:
create a lens for this property.
We can update our Lens<> class to add the New() static method and its helper methods:
class Lens<TObject, TProperty>
{
public Func<TObject, TProperty> Get {get; private set;}
public Action<TObject, TProperty> Set {get; private set;}
public Lens(Func<TObject, TProperty> getter, Action<TObject, TProperty> setter)
{
Get = getter;
Set = setter;
}
<summary>
Creates a new lens by taking the property-getter function, and deriving from
that, the property-setter function, returning a new lens with both the
getter/setter filled-in.
</summary>
public static Lens<TObject,TProperty>
New(Expression<Func<TObject, TProperty>> propertyGetter)
{
var getter = propertyGetter.Compile();
var setter = DeriveSetterLambda(propertyGetter);
var lens = new Lens<TObject, TProperty>(getter, setter);
return lens;
}
<summary>
Derives the property-setter function from the supplied property-getter function.
</summary>
static Action<TObject,TProperty>
DeriveSetterLambda(Expression<Func<TObject, TProperty>> propertyGetter)
{
string propName = GetPropertyName(propertyGetter);
var paramTargetExp = Expression.Parameter(typeof(TObject));
var paramValueExp = Expression.Parameter(typeof(TProperty));
var propExp = Expression.PropertyOrField(paramTargetExp , propName);
var assignExp = Expression.Assign(propExp, paramValueExp);
var setter = Expression.Lambda<Action<TObject,TProperty>>(assignExp, paramTargetExp, paramValueExp);
return setter.Compile();
}
<summary>
Get the name of the property mentioned in the property-getter function as a
string.
</summary>
static string GetPropertyName(Expression<Func<TObject,TProperty>> exp)
{
var propExp = (MemberExpression)exp.Body;
var n = propExp.Member.Name;
return n;
}
}
The New() method creates a new Lens<> by taking the getter-function, and deriving from it the corresponding setter-function for the lens. To make this happen we use C# Expression Trees to do a little meta-programming and represent code as data and generate code on the fly while the application is running.
We can now rewrite our Main() method to take advantage of the Lens<>.New() method to create lenses:
void Main()
{
// lens for the Name property using the New() method
var name = Lens<Person,string>.New(p => p.Name);
// lens for the Age property using the New() method
var age = Lens<Person,int>.New(p => p.Age);
var person = new Person {Name = "Peter", Age = 30};
name.Get(person).Dump(); // output: Peter
age.Get(person).Dump(); // output: 30
name.Set(person, "Michael");
name.Get(person).Dump(); // output: Michael
}
We now have a simple implementation of lenses which we can use in C#. In the next post, I will talk more about the interresting uses of lenses.
